diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 6c7d459f9..9e35904ae 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -394,9 +394,8 @@ class Impact: mfl = re.match(r'^(.+?):(\d+)(?::\d+)?$', s) if mfl and ('/' in mfl.group(1) or '.' in mfl.group(1)) and kind in (None, 'field', 'type'): f, ln = self.g.repo_file(mfl.group(1))[0], int(mfl.group(2)) - files = sorted({x['file'] for x in self.g.q("SELECT DISTINCT file FROM symbols WHERE file = ? OR file LIKE ?", f, f"%/{f}") - if x['file'] == f or (x['file'] or '').endswith('/' + f)}) - if f in files: files = [f] + files, scoped = self.g.line_files(f) # --in chooses among a partial path's files (#1666) + if scoped and len(files) == 1: s = f"{files[0]}:{ln}" d = self.g.decl_at_line(files[0], ln) if len(files) == 1 else None if d and d[0] == 'field' and kind in (None, 'field'): row = self.fields.get(d[1]['rowid']) or d[1] @@ -568,8 +567,10 @@ class Impact: # the one declaration a file:line could not target, and its bare name answered for every const so named fl = re.fullmatch(r'(.+\.\w+):(\d+)', s) if fl: - p, n = fl.group(1), int(fl.group(2)) - return [f for f in self.fields.values() if f['line'] == n and (f['file'] == p or (f['file'] or '').endswith('/' + p))] + # a partial path names ONE file, as for a callable (#1599, #1666): matched against every file it ends, it took + # the field at that line in whichever of them had one, so `test.js:6` in 17 test.js answered for one of them + files, _ = self.g.line_files(self.g.repo_file(fl.group(1))[0]); n = int(fl.group(2)) + return [f for f in self.fields.values() if f['line'] == n and f['file'] == files[0]] if len(files) == 1 else [] parts = s.split('.'); name = parts[-1]; owner = '.'.join(parts[:-1]) rows = [f for f in self.fields.values() if f['name'] == name and (not owner or (f['owner'] or '') == owner or (f['owner'] or '').endswith('.' + owner) or (f['qualified_name'] or '').endswith('.' + s))] if not rows and '.' in s: # the dotted spelling, only after the written one diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 754c27d18..c57d48bed 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -324,6 +324,18 @@ class G: try: with open(os.path.join(self.repo, f), 'rb') as h: return len(h.read().splitlines()) except OSError: return None + + def line_files(self, f): + """-> (the indexed files a file:line's path `f` names, the --in note when the scope chose among them). The file + written in full, else every file `f` is a suffix of. --IN CHOOSES AMONG THEM (#1666): it was applied only after + the file was settled, so `index.js:2 --in packages/a` was refused with every index.js listed. Under the scope + one file is the answer; two or more are the refusal's list; none leaves the list whole, and says so.""" + files = sorted({x['file'] for x in self.q("SELECT DISTINCT file FROM symbols WHERE file = ? OR file LIKE ?", f, f"%/{f}") + if x['file'] == f or (x['file'] or '').endswith('/' + f)}) + if f in files: return [f], '' + if len(files) < 2 or not self.IN: return files, '' + kept = [x for x in files if self.under_in(x)] + return (kept, f" under *{self.IN}*") if kept else (files, f", none of them under *{self.IN}*") def _resolve(self, sel, fragment=False): s = sel.strip().strip('`"\'') # file:line, or file:line:col as a stack trace prints it — the column never changes which callable spans the line @@ -334,17 +346,16 @@ class G: # lib/ took the smallest span across BOTH files, so the answer was for whichever function at that line was # shorter, labelled with what was typed. The path is settled first: the file written in full, else the one # file it is a suffix of; two or more are listed, each with what spans the line there. - files = sorted({x['file'] for x in self.q("SELECT DISTINCT file FROM symbols WHERE file = ? OR file LIKE ?", f, f"%/{f}") - if x['file'] == f or (x['file'] or '').endswith('/' + f)}) - if f in files: files = [f] + files, scoped = self.line_files(f) if len(files) > 1: rows = [] for fx in files[:12]: at = self.q("SELECT id FROM symbols WHERE file = ? AND line <= ? AND end_line >= ? AND method_id IS NOT NULL AND kind <> 'module' ORDER BY end_line - line LIMIT 1", fx, ln, ln) rows.append(f" {fx}:{ln} " + (self.disp(at[0]['id']) if at else '(top-level code)')) if len(files) > 12: rows.append(f" … +{len(files) - 12} more") - die(f"'{m.group(1)}' matches {len(files)} indexed files — give enough of the path to name one:\n" + '\n'.join(rows)) + die(f"'{m.group(1)}' matches {len(files)} indexed files{scoped} — give enough of the path to name one:\n" + '\n'.join(rows)) if files: f = files[0] + if scoped: s = f"{f}:{ln}" # the scope chose the file: the label names it # A LINE OUTSIDE THE FILE IS REFUSED (#1601). Line 0, or a line past the end — a number copied before the file # was edited, or from an old stack trace — spans no callable, so it fell through to the module node and was # answered as the file's top-level code, as if that line existed. The file on disk is what the reader sees. diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/case.json b/tests/cases/javascript/fileline-partial-path-in-scope/case.json new file mode 100644 index 000000000..e0085d4de --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/case.json @@ -0,0 +1,48 @@ +{"lang": "javascript", + "checks": [ + {"why": "a basename matching five indexed files, one of them under --in, answers for that one and names it (#1666)", + "run": ["impact", "index.js:2", "--in", "packages/a"], + "want": ["change: runA (at packages/a/src/index.js:2)", "startA"], + "avoid": ["matches 5 indexed files", "startB", "runB"]}, + {"why": "the same through path, from everything into the one file under --in", + "run": ["path", "*", "index.js:2", "--in", "packages/b"], + "want": ["startB"], + "avoid": ["matches 5 indexed files", "startA"]}, + {"why": "a dir/file suffix is narrowed the same way", + "run": ["impact", "src/index.js:2", "--in", "packages/b"], + "want": ["change: runB (at packages/b/src/index.js:2)", "startB"], + "avoid": ["matches 2 indexed files", "startA"]}, + {"why": "control: a scope that still leaves two files is refused, listing only those two", + "run": ["impact", "index.js:2", "--in", "packages/c"], + "expect_error": true, + "want": ["matches 2 indexed files under *packages/c*", "packages/c/index.js:2", "packages/c/lib/index.js:2"], + "avoid": ["packages/a/src/index.js", "change: load"]}, + {"why": "control: the same through path", + "run": ["path", "*", "index.js:2", "--in", "packages/c"], + "expect_error": true, + "want": ["matches 2 indexed files under *packages/c*"], + "avoid": ["packages/d/index.js"]}, + {"why": "control: a scope holding none of the files is refused with every file listed", + "run": ["impact", "index.js:2", "--in", "shared"], + "expect_error": true, + "want": ["matches 5 indexed files", "none of them under *shared*", "packages/a/src/index.js:2", "packages/d/index.js:2"]}, + {"why": "control: without --in the basename is refused as before (#1599)", + "run": ["impact", "index.js:2"], + "expect_error": true, + "want": ["matches 5 indexed files", "packages/a/src/index.js:2", "packages/c/lib/index.js:2"]}, + {"why": "control: the full path with --in answers for it, labelled as typed", + "run": ["impact", "packages/a/src/index.js:2", "--in", "packages/a"], + "want": ["change: runA (at packages/a/src/index.js:2)", "startA"], + "avoid": ["startB"]}, + {"why": "a const on the line of a partial path matching two files is refused, not answered for one of them", + "run": ["impact", "config.js:1"], + "expect_error": true, + "want": ["matches 2 indexed files", "packages/a/src/config.js:1", "packages/b/src/config.js:1"], + "avoid": ["change: variable tokenA", "change: const tokenA"]}, + {"why": "the same const line with --in answers for the one file under the scope", + "run": ["impact", "config.js:1", "--in", "packages/b"], + "want": ["tokenB"], + "avoid": ["tokenA", "matches 2 indexed files"]}, + {"why": "control: a partial path matching one file is labelled as typed, with or without --in", + "run": ["impact", "d/index.js:2", "--in", "packages"], + "want": ["change: main (at d/index.js:2)"]}]} diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/app.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/app.js new file mode 100644 index 000000000..df44eb3b9 --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/app.js @@ -0,0 +1,7 @@ +const { runA } = require('./index'); + +function startA() { + return runA(1); +} + +module.exports = { startA }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/config.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/config.js new file mode 100644 index 000000000..77db3fb2b --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/config.js @@ -0,0 +1,2 @@ +const tokenA = 'a'; +module.exports = { tokenA }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/index.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/index.js new file mode 100644 index 000000000..535663afa --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/a/src/index.js @@ -0,0 +1,4 @@ +function runA(x) { + return String(x); +} +module.exports = { runA }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/app.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/app.js new file mode 100644 index 000000000..e3b3f5f9f --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/app.js @@ -0,0 +1,7 @@ +const { runB } = require('./index'); + +function startB() { + return runB('1'); +} + +module.exports = { startB }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/config.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/config.js new file mode 100644 index 000000000..d0abade26 --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/config.js @@ -0,0 +1,2 @@ +const tokenB = 'b'; +module.exports = { tokenB }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/index.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/index.js new file mode 100644 index 000000000..04b42876d --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/b/src/index.js @@ -0,0 +1,4 @@ +function runB(x) { + return Number(x); +} +module.exports = { runB }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/index.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/index.js new file mode 100644 index 000000000..c6c4483ec --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/index.js @@ -0,0 +1,4 @@ +function load() { + return 1; +} +module.exports = { load }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/lib/index.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/lib/index.js new file mode 100644 index 000000000..a6bf9045a --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/c/lib/index.js @@ -0,0 +1,4 @@ +function loadLib() { + return 2; +} +module.exports = { loadLib }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/packages/d/index.js b/tests/cases/javascript/fileline-partial-path-in-scope/packages/d/index.js new file mode 100644 index 000000000..3addcf4e5 --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/packages/d/index.js @@ -0,0 +1,4 @@ +function main() { + return 3; +} +module.exports = { main }; diff --git a/tests/cases/javascript/fileline-partial-path-in-scope/shared/util.js b/tests/cases/javascript/fileline-partial-path-in-scope/shared/util.js new file mode 100644 index 000000000..eaa593faa --- /dev/null +++ b/tests/cases/javascript/fileline-partial-path-in-scope/shared/util.js @@ -0,0 +1,4 @@ +function helper() { + return 4; +} +module.exports = { helper };