diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index bccca526a..948c6f3de 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -319,6 +319,11 @@ class G: if p == root or p.startswith(root.rstrip('/') + '/'): return os.path.relpath(p, root).replace(os.sep, '/'), False return f.lstrip('/'), True + def file_lines(self, f): + """how many lines the indexed file `f` has on disk, or None when it cannot be read there""" + try: + with open(os.path.join(self.repo, f), 'rb') as h: return len(h.read().splitlines()) + except OSError: return None 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 @@ -340,6 +345,12 @@ class G: 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)) if files: f = files[0] + # 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. + if ln < 1: die(f"line {ln} is not in {f}: lines are numbered from 1") + n = self.file_lines(f) if files else None + if n is not None and ln > n: die(f"{f} has {n} line{'' if n == 1 else 's'}; line {ln} is not in it") # the smallest callable(s) spanning the line — several when they share it (`class Item { run(){} tag(){} }`); # top-level code belongs to the file's module node, which is a caller like any other r = self.q("SELECT id, end_line - line AS span FROM symbols WHERE file = ? AND line <= ? AND end_line >= ? AND method_id IS NOT NULL AND kind <> 'module' ORDER BY span", f, ln, ln) diff --git a/tests/cases/javascript/fileline-line-outside-file/case.json b/tests/cases/javascript/fileline-line-outside-file/case.json new file mode 100644 index 000000000..2dae3ef39 --- /dev/null +++ b/tests/cases/javascript/fileline-line-outside-file/case.json @@ -0,0 +1,42 @@ +{"lang": "javascript", + "checks": [ + {"why": "a line past the end of the file is refused with the file's length, not answered as its top-level code (#1601)", + "run": ["impact", "src/services/userService.js:99"], + "expect_error": true, + "want": ["src/services/userService.js has 4 lines; line 99 is not in it"], + "avoid": [""]}, + {"why": "line 0 is refused too", + "run": ["impact", "src/services/userService.js:0"], + "expect_error": true, + "want": ["line 0 is not in src/services/userService.js"], + "avoid": [""]}, + {"why": "the same through path", + "run": ["path", "*", "src/services/userService.js:0"], + "expect_error": true, + "want": ["line 0 is not in src/services/userService.js"], + "avoid": ["0 method(s)"]}, + {"why": "the line just past the end, through path, with a partial undotted file name", + "run": ["path", "*", "services/userService.js:5"], + "expect_error": true, + "want": ["src/services/userService.js has 4 lines; line 5 is not in it"], + "avoid": ["0 method(s)"]}, + {"why": "a file with no newline at its end: the line after its last is refused", + "run": ["impact", "app.js:3"], + "expect_error": true, + "want": ["src/app.js has 2 lines; line 3 is not in it"]}, + {"why": "control: the last line of a file with no newline at its end is still its top-level code", + "run": ["impact", "src/app.js:2"], + "want": ["app. (at src/app.js:2)"], + "avoid": ["is not in it"]}, + {"why": "control: the last line, top-level code, still answers as the module", + "run": ["impact", "src/services/userService.js:4"], + "want": ["userService. (at src/services/userService.js:4)"], + "avoid": ["is not in it"]}, + {"why": "control: a line inside a function still answers for that function, with its caller", + "run": ["path", "*", "src/services/userService.js:2"], + "want": ["getUser"], + "avoid": ["is not in it"]}, + {"why": "control: a file the index does not hold is still refused as before", + "run": ["impact", "zzz/userService.js:99"], + "expect_error": true, + "want": ["no callable spans zzz/userService.js:99"]}]} diff --git a/tests/cases/javascript/fileline-line-outside-file/src/app.js b/tests/cases/javascript/fileline-line-outside-file/src/app.js new file mode 100644 index 000000000..d61a3b3a4 --- /dev/null +++ b/tests/cases/javascript/fileline-line-outside-file/src/app.js @@ -0,0 +1,2 @@ +const { getUser } = require('./services/userService'); +getUser(1); \ No newline at end of file diff --git a/tests/cases/javascript/fileline-line-outside-file/src/db.js b/tests/cases/javascript/fileline-line-outside-file/src/db.js new file mode 100644 index 000000000..e764e07df --- /dev/null +++ b/tests/cases/javascript/fileline-line-outside-file/src/db.js @@ -0,0 +1 @@ +exports.find = function (id) { return { id }; }; diff --git a/tests/cases/javascript/fileline-line-outside-file/src/services/userService.js b/tests/cases/javascript/fileline-line-outside-file/src/services/userService.js new file mode 100644 index 000000000..bcba4fe0f --- /dev/null +++ b/tests/cases/javascript/fileline-line-outside-file/src/services/userService.js @@ -0,0 +1,4 @@ +const db = require('../db'); +function getUser(id) { return db.find(id); } +module.exports = { getUser }; +// last line