//! What a file declares, so a merge never makes two of one thing: the //! functions, classes, types and members code declares (named with where //! they sit), and the keys data files set. //! //! It reads lines, not a full grammar, and it's only ever used to turn a //! merge into a conflict: a merged file that declares something more times //! than either side did (two `def helper` in one module, `timeout:` twice in //! one YAML map, two `"test"` scripts in one JSON object) isn't a clean merge, //! even where git would call it one. use std::collections::HashMap; #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Family { Python, /// JavaScript and TypeScript. Script, Rust, Go, /// Java, Kotlin, Scala, C#, Swift, C and C++: members named by signature (overloads differ). Signature, Yaml, /// .env and .properties: `KEY=value`. Env, /// .ini, .cfg, .conf: keys in [sections]. Ini, Json, Toml, Other, } fn family(path: &str) -> Family { let name = path.rsplit('/').next().unwrap_or(path).to_ascii_lowercase(); if name == ".env" || name.starts_with(".env.") || name.ends_with(".env") { return Family::Env; } let ext = name.rsplit_once('.').map(|(_, e)| e).unwrap_or(""); match ext { "py" | "pyi" => Family::Python, "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" | "mts" | "cts" => Family::Script, "rs" => Family::Rust, "go" => Family::Go, "java" | "kt" | "kts" | "scala" | "cs" | "swift" | "c" | "h" | "cc" | "cpp" | "cxx" | "hpp" | "hh" | "m" | "mm" => Family::Signature, "yml" | "yaml" => Family::Yaml, "properties" => Family::Env, "ini" | "cfg" | "conf" => Family::Ini, "json" => Family::Json, "toml" => Family::Toml, _ => Family::Other, } } /// Every declaration or key in `text`, as (where it sits, line number). pub fn declared(path: &str, text: &[u8]) -> Vec<(String, usize)> { let Ok(text) = std::str::from_utf8(text) else { return Vec::new() }; match family(path) { Family::Python => python(text), Family::Script | Family::Rust | Family::Go | Family::Signature => braces(family(path), text), Family::Yaml => yaml(text), Family::Env => env(text), Family::Ini => ini(text), Family::Json => json(text), Family::Toml => toml(text), Family::Other => Vec::new(), } } /// What `merged` declares more times than `ours` or `theirs` did alone. pub fn new_duplicates(path: &str, ours: &[u8], theirs: &[u8], merged: &[u8]) -> Vec { if family(path) == Family::Other { return Vec::new(); } let count = |text: &[u8]| { let mut m: HashMap = HashMap::new(); for (k, _) in declared(path, text) { *m.entry(k).or_default() += 1; } m }; let (o, t, m) = (count(ours), count(theirs), count(merged)); let mut out: Vec = m .into_iter() .filter(|(k, n)| *n > 1 && *n > o.get(k).copied().unwrap_or(0).max(t.get(k).copied().unwrap_or(0))) .map(|(k, _)| k) .collect(); out.sort(); out } /// The lines of `text` that declare `key`. pub fn lines_of(path: &str, text: &[u8], key: &str) -> Vec { declared(path, text).into_iter().filter(|(k, _)| k == key).map(|(_, l)| l).collect() } /// The last part of a key, for messages (`Shop.total` → `total`). pub fn short(key: &str) -> &str { key.rsplit_once('.').map(|(_, n)| n).unwrap_or(key) } fn ident(s: &str) -> Option<&str> { let end = s.find(|c: char| !(c.is_alphanumeric() || c == '_' || c == '$')).unwrap_or(s.len()); (end > 0 && !s.as_bytes()[0].is_ascii_digit()).then(|| &s[..end]) } /// `= {` or `: {` (an object literal or a typed one), not `= () => {`. fn opens_object(tail: &str) -> bool { let rest = tail.trim_start_matches(['=', ':']).trim(); rest == "{" || (tail.starts_with(':') && rest.ends_with("= {")) } fn after<'a>(s: &'a str, word: &str) -> Option<&'a str> { let rest = s.strip_prefix(word)?; (rest.starts_with(char::is_whitespace)).then(|| rest.trim_start()) } // Python: indentation says where a def or class sits. fn python(text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); let mut stack: Vec<(usize, String)> = Vec::new(); let mut in_string: Option<&str> = None; for (n, line) in text.lines().enumerate() { let trimmed = line.trim_start(); // Skip docstrings and other triple-quoted text. if let Some(q) = in_string { if trimmed.contains(q) { in_string = None; } continue; } for q in ["\"\"\"", "'''"] { if trimmed.matches(q).count() % 2 == 1 { in_string = Some(q); } } if trimmed.is_empty() || trimmed.starts_with('#') { continue; } let indent = line.len() - trimmed.len(); while stack.last().is_some_and(|(i, _)| *i >= indent) { stack.pop(); } let s = trimmed.strip_prefix("async ").map(str::trim_start).unwrap_or(trimmed); let name = after(s, "def").or_else(|| after(s, "class")).and_then(ident); let assigned = (indent == 0 || stack.last().is_some_and(|(_, k)| k.starts_with("class "))).then(|| { // A module or class attribute: NAME = ... or NAME: type = ... let id = ident(s)?; let rest = s[id.len()..].trim_start(); ((rest.starts_with('=') && !rest.starts_with("==")) || rest.starts_with(':')).then_some(id) }); let parent = stack.iter().map(|(_, k)| k.split_once(' ').map(|(_, n)| n).unwrap_or(k)).collect::>().join("."); let qualified = |name: &str| if parent.is_empty() { name.to_string() } else { format!("{parent}.{name}") }; if let Some(name) = name { out.push((qualified(name), n)); let kind = if s.starts_with("class") { "class" } else { "def" }; stack.push((indent, format!("{kind} {name}"))); } else if let Some(Some(id)) = assigned && id != "_" { out.push((qualified(id), n)); } } out } // Brace languages: depth says where a declaration sits; strings and comments are skipped. /// A line with strings' contents and comments taken out, so braces and words /// in them don't count. `block` carries a /* comment */ across lines. fn code_of(line: &str, block: &mut bool, raw_string: &mut bool) -> String { let mut out = String::with_capacity(line.len()); let b = line.as_bytes(); let mut i = 0; while i < b.len() { if *block { if b[i..].starts_with(b"*/") { *block = false; i += 2; } else { i += 1; } continue; } if *raw_string { if b[i] == b'`' { *raw_string = false; out.push('`'); } i += 1; continue; } match b[i] { b'/' if b.get(i + 1) == Some(&b'/') => break, b'/' if b.get(i + 1) == Some(&b'*') => { *block = true; i += 2; } b'`' => { *raw_string = true; out.push('`'); i += 1; } q @ (b'"' | b'\'') => { // A lifetime or char-like single quote with no closing quote nearby is kept. let close = b[i + 1..].iter().enumerate().scan(false, |esc, (k, &c)| { let hit = !*esc && c == q; *esc = !*esc && c == b'\\'; Some((k, hit)) }); match close.filter(|(_, hit)| *hit).map(|(k, _)| k).next() { Some(k) => { out.push(q as char); out.push(q as char); i += k + 2; } None => { out.push(q as char); i += 1; } } } c => { out.push(c as char); i += 1; } } } out } const MODIFIERS: &[&str] = &[ "export", "default", "pub", "pub(crate)", "pub(super)", "pub(self)", "async", "unsafe", "static", "public", "private", "protected", "internal", "final", "abstract", "override", "open", "extern", "inline", "virtual", "sealed", "readonly", "declare", "partial", "fileprivate", "mutating", "lateinit", "suspend", "operator", "infix", "data", "inner", "companion", "synchronized", "native", "transient", "volatile", "strictfp", "constexpr", "explicit", "friend", "new", ]; fn strip_modifiers(mut s: &str) -> &str { loop { let before = s; while s.starts_with('@') || s.starts_with('#') { // Annotations and attributes on the same line (@Override, #[inline]). let end = s.find(char::is_whitespace).unwrap_or(s.len()); if s.starts_with("#[") { match s.find(']') { Some(e) => s = s[e + 1..].trim_start(), None => return "", } } else { s = s[end..].trim_start(); } } for m in MODIFIERS { if let Some(rest) = after(s, m) { s = rest; } } if s == before { return s; } } } const CONTROL: &[&str] = &[ "if", "else", "for", "while", "switch", "match", "return", "catch", "do", "try", "throw", "case", "when", "loop", "using", "lock", "yield", "await", "guard", "defer", "go", "select", "sizeof", "typeof", "delete", "assert", "print", "println", ]; /// What a line declares, in a brace language: (name, whether it holds members). fn brace_decl(fam: Family, s: &str, parent_holds_members: bool) -> Option<(String, bool)> { let s = strip_modifiers(s.trim()); let word = |w: &str| after(s, w).and_then(ident).map(String::from); let first = ident(s).unwrap_or(""); if CONTROL.contains(&first) { return None; } match fam { Family::Script => { if let Some(rest) = after(s, "function").or_else(|| s.strip_prefix("function*").map(str::trim_start)) { return ident(rest.trim_start_matches('*').trim_start()).map(|n| (n.to_string(), false)); } for (w, holds) in [("class", true), ("interface", true), ("enum", true), ("namespace", true), ("module", true), ("type", false)] { if let Some(n) = word(w) { return Some((n, holds)); } } for w in ["const", "let", "var"] { if let Some(rest) = after(s, w) && let Some(n) = ident(rest) { let tail = rest[n.len()..].trim_start(); if tail.starts_with('=') || tail.starts_with(':') || tail.starts_with(';') || tail.is_empty() { // An object assigned here holds members (a function assigned here has a body). return Some((n.to_string(), opens_object(tail))); } } } if parent_holds_members { // Members: name(, get name(, name: ..., name = ..., #name, "quoted": ... let s2 = s.strip_prefix("get ").or_else(|| s.strip_prefix("set ")).map(|r| r.trim_start()).unwrap_or(s); let prefix = if s2.len() != s.len() { &s[..3] } else { "" }; let s2 = s2.trim_start_matches('#').trim_start_matches('*'); let (name, tail) = if let Some(q) = s2.strip_prefix('"').or_else(|| s2.strip_prefix('\'')) { let end = q.find(['"', '\''])?; (q[..end].to_string(), q[end + 1..].trim_start()) } else { let n = ident(s2)?; (n.to_string(), s2[n.len()..].trim_start()) }; let tail = tail.trim_start_matches('?').trim_start_matches('!'); if tail.starts_with('(') || tail.starts_with('<') || tail.starts_with(':') || (tail.starts_with('=') && !tail.starts_with("==")) { return Some((format!("{prefix}{name}"), opens_object(tail))); } } None } Family::Rust => { let s = s.strip_prefix("const ").filter(|r| r.trim_start().starts_with("fn ")).unwrap_or(s); if let Some(n) = word("fn") { return Some((n, false)); } if let Some(rest) = s.strip_prefix("macro_rules!") { return ident(rest.trim_start()).map(|n| (format!("{n}!"), false)); } for (w, holds) in [("struct", true), ("enum", true), ("trait", true), ("union", true), ("mod", true), ("type", false), ("static", false)] { if let Some(n) = word(w) { return Some((n, holds)); } } if let Some(n) = word("const").filter(|n| n != "fn") { return Some((n, false)); } if first == "impl" { let header = s.split('{').next().unwrap_or(s).split_whitespace().collect::>().join(" "); return Some((header, true)); } if parent_holds_members { // Fields (name: Type) and variants (Name, Name(..), Name { .. }). let n = ident(s)?; let tail = s[n.len()..].trim_start(); if (tail.starts_with(':') && !tail.starts_with("::")) || tail.starts_with(',') || tail.starts_with('(') || tail.starts_with('{') || tail.is_empty() || tail.starts_with('=') { return Some((n.to_string(), false)); } } None } Family::Go => { if let Some(rest) = after(s, "func") { if let Some(recv) = rest.strip_prefix('(') { let close = recv.find(')')?; let ty = recv[..close].split_whitespace().last()?.trim_start_matches('*'); let ty = ty.split('[').next().unwrap_or(ty); let n = ident(recv[close + 1..].trim_start())?; return Some((format!("{ty}.{n}"), false)); } return ident(rest).map(|n| (n.to_string(), false)); } for w in ["type", "var", "const"] { if let Some(n) = word(w) { let holds = w == "type" && (s.contains(" struct") || s.contains(" interface")); return Some((n, holds)); } } if parent_holds_members { let n = ident(s)?; let tail = &s[n.len()..]; if tail.starts_with(char::is_whitespace) && !tail.trim().is_empty() { return Some((n.to_string(), false)); } } None } Family::Signature => { for w in ["class", "interface", "enum", "struct", "record", "object", "protocol", "extension", "namespace", "trait", "union", "actor"] { // A type only where its body follows (`struct strbuf *sb` names one, it doesn't declare it). if let Some(rest) = after(s, w) && let Some(n) = ident(rest) { let tail = rest[n.len()..].trim_start(); let opens = tail.is_empty() || tail.starts_with('{') || (tail.starts_with(':') && !tail.starts_with("::")) || tail.starts_with('<') || tail.starts_with('(') || ["extends", "implements", "where", "final", "permits"].iter().any(|k| tail.starts_with(k)); if opens { return Some((n.to_string(), true)); } } } for w in ["val", "var", "let", "func", "fun", "def", "init", "deinit"] { if let Some(rest) = after(s, w).or_else(|| (s == w || s.starts_with(&format!("{w}("))).then(|| &s[w.len()..])) { if w == "func" || w == "fun" || w == "def" || w == "init" { // Overloads differ by parameters: the whole signature names it. let sig = rest.split('{').next().unwrap_or(rest).split_whitespace().collect::>().join(" "); return Some((format!("{w} {sig}"), false)); } return ident(rest).map(|n| (n.to_string(), false)); } } if !parent_holds_members { return None; } // A method or function: ` name(params)` with a body (or ending a prototype in a class). if let Some(open) = s.find('(') { let head = s[..open].trim_end(); let name = head.rsplit(|c: char| !(c.is_alphanumeric() || c == '_' || c == '~' || c == ':')).next()?; if name.is_empty() || CONTROL.contains(&name) { return None; } let close = s.rfind(')')?; let tail = s[close + 1..].trim(); let body = tail.ends_with('{') || tail.is_empty() || tail.starts_with("const") || tail.starts_with("throws") || tail.starts_with("->") || tail.starts_with("where") || tail.starts_with(':'); if !body && !(parent_holds_members && tail.ends_with(';')) { return None; } let sig = s[..=close].split_whitespace().collect::>().join(" "); return Some((sig, false)); } if parent_holds_members && s.ends_with(';') { // A field: the name before = or ;. let decl = s.split('=').next().unwrap_or(s).trim_end_matches(';').trim(); let n = decl.rsplit(|c: char| !(c.is_alphanumeric() || c == '_')).next()?; if !n.is_empty() && decl.contains(char::is_whitespace) { return Some((n.to_string(), false)); } } None } _ => None, } } fn braces(fam: Family, text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); // Open braces: the name of what each one opened, and whether it holds members. let mut opened: Vec> = Vec::new(); let mut pending: Option<(String, bool)> = None; let (mut block, mut raw) = (false, false); for (n, line) in text.lines().enumerate() { let code = code_of(line, &mut block, &mut raw); let trimmed = code.trim(); if trimmed.is_empty() { continue; } let inside = opened.last().map(|o| o.as_ref().is_some_and(|(_, holds)| *holds)).unwrap_or(fam == Family::Signature && opened.is_empty()); let in_body = opened.iter().any(|o| o.as_ref().is_some_and(|(_, holds)| !*holds)); let starts_with_close = trimmed.starts_with('}'); // A prototype at the top of a C file (`int f(void);`) may be repeated; only definitions count there. let prototype = fam == Family::Signature && opened.is_empty() && trimmed.ends_with(';'); let decl = if starts_with_close || prototype || (in_body && fam != Family::Script && fam != Family::Rust) { None } else { brace_decl(fam, trimmed, inside) }; let qualified = decl.as_ref().map(|(name, holds)| { let path = opened.iter().filter_map(|o| o.as_ref().map(|(k, _)| k.as_str())).collect::>().join("."); let key = if path.is_empty() { name.clone() } else { format!("{path}.{name}") }; (key, *holds) }); if let Some((key, _)) = &qualified { out.push((key.clone(), n)); } if qualified.is_some() { pending = qualified.clone(); } // A brace closed on its own line (`= {}`, `{ a, b }`) isn't the one that // opens a declaration's body: that's the first one still open at the // end of the line. Unnamed blocks (callbacks, ifs) are scopes of their own. let chars: Vec = trimmed.chars().collect(); let mut closes_here = vec![false; chars.len()]; let mut open_at: Vec = Vec::new(); for (i, c) in chars.iter().enumerate() { match c { '{' => open_at.push(i), '}' => { if let Some(o) = open_at.pop() { closes_here[o] = true; } } _ => {} } } for (i, c) in chars.iter().enumerate() { match c { '{' if closes_here[i] => opened.push(None), '{' => opened.push(Some(pending.take().unwrap_or_else(|| (format!("@{n}"), false)))), '}' => { opened.pop(); } ';' => pending = None, _ => {} } } // A declaration whole on one line (`function a() {}`) names no later block. if trimmed.ends_with('}') { pending = None; } } out } // Data files. /// YAML mapping keys, by where they sit (indent and the keys above them). fn yaml(text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); // (indent, path to here, list item number when inside one) let mut stack: Vec<(usize, String)> = Vec::new(); let mut block_scalar: Option = None; let mut item = 0usize; for (n, line) in text.lines().enumerate() { let trimmed = line.trim_start(); let indent = line.len() - trimmed.len(); if let Some(i) = block_scalar { if trimmed.is_empty() || indent > i { continue; } block_scalar = None; } if trimmed.is_empty() || trimmed.starts_with('#') { continue; } if trimmed.starts_with("---") || trimmed.starts_with("...") { stack.clear(); item += 1; continue; } let (indent, body) = if let Some(rest) = trimmed.strip_prefix("- ") { // A list item starts a mapping of its own. item += 1; while stack.last().is_some_and(|(i, _)| *i >= indent) { stack.pop(); } let parent = stack.last().map(|(_, p)| p.clone()).unwrap_or_default(); stack.push((indent, format!("{parent}[{item}]"))); (indent + 2 + (rest.len() - rest.trim_start().len()), rest.trim_start()) } else { (indent, trimmed) }; let key = if let Some(q) = body.strip_prefix('"').or_else(|| body.strip_prefix('\'')) { q.find(['"', '\'']).map(|e| (&q[..e], q[e + 1..].trim_start())) } else { // The colon that ends a key has a space or the line's end after it (`test:linux:` is one key). let b = body.as_bytes(); (0..b.len()).find(|&i| b[i] == b':' && (i + 1 == b.len() || b[i + 1].is_ascii_whitespace())).map(|e| (body[..e].trim_end(), &body[e..])) }; let Some((key, tail)) = key else { continue }; if !tail.starts_with(':') || !(tail.len() == 1 || tail[1..].starts_with(char::is_whitespace)) || key.is_empty() || key.contains(' ') && !body.starts_with(['"', '\'']) { continue; } while stack.last().is_some_and(|(i, _)| *i >= indent) { stack.pop(); } let parent = stack.last().map(|(_, p)| p.clone()).unwrap_or_default(); let path = if parent.is_empty() { key.to_string() } else { format!("{parent}.{key}") }; out.push((path.clone(), n)); stack.push((indent, path)); let value = tail[1..].trim(); if value.starts_with('|') || value.starts_with('>') { block_scalar = Some(indent); } } out } fn env(text: &str) -> Vec<(String, usize)> { text.lines() .enumerate() .filter_map(|(n, line)| { let s = line.trim(); if s.is_empty() || s.starts_with('#') || s.starts_with('!') { return None; } let s = s.strip_prefix("export ").map(str::trim_start).unwrap_or(s); let end = s.find(['=', ':'])?; let key = s[..end].trim(); (!key.is_empty() && !key.contains(char::is_whitespace)).then(|| (key.to_string(), n)) }) .collect() } fn ini(text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); let mut section = String::new(); for (n, line) in text.lines().enumerate() { let s = line.trim(); if s.is_empty() || s.starts_with('#') || s.starts_with(';') { continue; } if s.starts_with('[') && s.ends_with(']') { section = s[1..s.len() - 1].trim().to_string(); continue; } if let Some(end) = s.find(['=', ':']) { let key = s[..end].trim(); if !key.is_empty() { out.push((format!("{section}.{key}"), n)); } } } out } /// JSON object keys, by the object they're in. fn json(text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); // Each open object or array: its path, and how many items an array has had. let mut stack: Vec<(String, Option)> = Vec::new(); let mut last_key: Option = None; let b = text.as_bytes(); let (mut i, mut line) = (0, 0); while i < b.len() { match b[i] { b'\n' => line += 1, b'"' => { let start = i + 1; let mut j = start; while j < b.len() && b[j] != b'"' { if b[j] == b'\\' { j += 1; } if b.get(j) == Some(&b'\n') { line += 1; } j += 1; } let s = String::from_utf8_lossy(&b[start..j.min(b.len())]).into_owned(); i = j; // A key: a string followed by a colon, inside an object. let mut k = i + 1; while k < b.len() && b[k].is_ascii_whitespace() { k += 1; } if b.get(k) == Some(&b':') && stack.last().is_some_and(|(_, arr)| arr.is_none()) { let parent = &stack.last().unwrap().0; out.push((format!("{parent}/{s}"), line)); last_key = Some(s); } } b'{' | b'[' => { let parent = stack.last().map(|(p, arr)| match arr { Some(n) => format!("{p}/{n}"), None => format!("{p}/{}", last_key.clone().unwrap_or_default()), }); stack.push((parent.unwrap_or_default(), (b[i] == b'[').then_some(0))); last_key = None; } b'}' | b']' => { stack.pop(); } b',' => { if let Some((_, Some(n))) = stack.last_mut() { *n += 1; } } _ => {} } i += 1; } out } /// TOML keys, by table (an array of tables, [[x]], starts a new one each time). fn toml(text: &str) -> Vec<(String, usize)> { let mut out = Vec::new(); let mut table = String::new(); let mut tables = 0; let mut in_string = false; for (n, line) in text.lines().enumerate() { let s = line.trim(); let was_string = in_string; if ["\"\"\"", "'''"].iter().any(|q| s.matches(q).count() % 2 == 1) { in_string = !in_string; } if was_string || s.is_empty() || s.starts_with('#') { continue; } if let Some(t) = s.strip_prefix("[[") { tables += 1; table = format!("{}#{tables}", t.trim_end_matches("]]").trim()); continue; } if let Some(t) = s.strip_prefix('[') { table = t.split(']').next().unwrap_or("").trim().to_string(); out.push((format!("[{table}]"), n)); continue; } if let Some(end) = s.find('=') { let key = s[..end].trim(); if !key.is_empty() && !key.starts_with(['{', '[', ',']) { out.push((format!("{table}.{key}"), n)); } } } out } #[cfg(test)] mod tests { use super::*; fn dups(path: &str, ours: &str, theirs: &str, merged: &str) -> Vec { new_duplicates(path, ours.as_bytes(), theirs.as_bytes(), merged.as_bytes()) } #[test] fn python_defs_by_place() { let ours = "def base():\n return 0\n\ndef helper(x):\n return x + 1\n"; let theirs = "def base():\n return 0\n\ndef helper(items):\n return len(items)\n"; let merged = "def base():\n return 0\n\ndef helper(x):\n return x + 1\n\ndef helper(items):\n return len(items)\n"; assert_eq!(dups("u.py", ours, theirs, merged), vec!["helper"]); // Two classes, each with its own __init__: not a duplicate. let a = "class A:\n def __init__(self):\n pass\n"; let ab = "class A:\n def __init__(self):\n pass\n\nclass B:\n def __init__(self):\n pass\n"; assert!(dups("u.py", a, a, ab).is_empty()); // A property and its setter on one side is what that side had. let p = "class A:\n @property\n def x(self):\n return 1\n\n @x.setter\n def x(self, v):\n pass\n"; assert!(dups("u.py", p, "class A:\n pass\n", p).is_empty()); // Module constants set twice. assert_eq!(dups("c.py", "A = 1\nT = 30\n", "A = 1\nT = 5\n", "A = 1\nT = 30\nT = 5\n"), vec!["T"]); } #[test] fn brace_languages() { let js = "export function two() {\n return 2;\n}\n"; assert_eq!(dups("a.js", js, js, &format!("{js}\n{js}")), vec!["two"]); let two = "function one() {\n const x = 1;\n return x;\n}\nfunction two() {\n const x = 2;\n return x;\n}\n"; assert!(dups("a.ts", two, two, two).is_empty()); assert!(declared("a.ts", two.as_bytes()).iter().any(|(k, _)| k == "one.x")); let class = "class Shop {\n total() {\n return 1;\n }\n}\n"; let class2 = "class Shop {\n total() {\n return 1;\n }\n total() {\n return 2;\n }\n}\n"; assert_eq!(dups("s.ts", class, class, class2), vec!["Shop.total"]); // Calls in a body aren't declarations. let calls = "function run() {\n log(1);\n log(2);\n}\n"; assert!(declared("r.js", calls.as_bytes()).iter().all(|(k, _)| k == "run")); let rs = "impl Shop {\n fn total(&self) -> u32 {\n 1\n }\n}\n"; let rs2 = "impl Shop {\n fn total(&self) -> u32 {\n 1\n }\n fn total(&self) -> u32 {\n 2\n }\n}\n"; assert_eq!(dups("s.rs", rs, rs, rs2), vec!["impl Shop.total"]); let go = "func (s *Shop) Total() int {\n\treturn 1\n}\n"; assert_eq!(dups("s.go", go, go, &format!("{go}{go}")), vec!["Shop.Total"]); // Java overloads differ by parameters. let java = "class A {\n void f(int x) {\n }\n void f(String s) {\n }\n}\n"; assert!(dups("A.java", java, java, java).is_empty()); let java2 = "class A {\n void f(int x) {\n }\n void f(int x) {\n }\n}\n"; assert_eq!(dups("A.java", "class A {\n void f(int x) {\n }\n}\n", "class A {\n void f(int x) {\n }\n}\n", java2), vec!["A.void f(int x)"]); // Strings and comments don't open blocks. let tricky = "const a = \"{\"; // {\nfunction b() {\n}\n"; assert_eq!(declared("t.js", tricky.as_bytes()).iter().map(|(k, _)| k.as_str()).collect::>(), vec!["a", "b"]); } #[test] fn data_keys() { let y = "server:\n port: 80\n timeout: 30\n"; let y2 = "server:\n port: 80\n timeout: 30\n timeout: 5\n"; assert_eq!(dups("c.yaml", y, y, y2), vec!["server.timeout"]); // The same key in two list items is fine. let list = "jobs:\n - name: a\n run: x\n - name: b\n run: y\n"; assert!(dups("ci.yml", list, list, list).is_empty()); // Block scalars are text. let block = "a: |\n b: 1\n b: 2\nc: 1\n"; assert!(dups("x.yml", block, block, block).is_empty()); assert_eq!(dups(".env", "A=1\n", "A=1\n", "A=1\nA=2\n"), vec!["A"]); let j = "{\n \"scripts\": {\n \"test\": \"jest\",\n \"test\": \"vitest\"\n }\n}\n"; assert_eq!(dups("package.json", "{}", "{}", j), vec!["/scripts/test"]); let arr = "[{\"a\": 1}, {\"a\": 2}]"; assert!(dups("x.json", arr, arr, arr).is_empty()); } }