From 4a8971fdf9c3782c0c7ab5b795e202d141d9fd04 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:13:36 -0700 Subject: [PATCH 1/4] dev: pin the engine packages back to 0.1.2, the version on npm 0.1.3 is not published, so `npm install` from dev silently skipped every engine package (they are optional) and the first index failed without a local souffle. The 0.1.3 notes stay in CHANGELOG.md under Unreleased. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- gemini-extension.json | 2 +- package.json | 12 ++++++------ plugins/axiomcode/.claude-plugin/plugin.json | 2 +- plugins/axiomcode/.codex-plugin/plugin.json | 2 +- plugins/axiomcode/.cursor-plugin/plugin.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c2acc00..02ad59a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Every release of `@axiomcode/code-graph` and its `@axiomcode/engine-` packages. The same notes are on each [GitHub release](https://github.com/AxiomCodeAI/axiomcodegraph/releases). -## 0.1.3 — 2026-09-26 +## Unreleased ### Documentation - #1372 README: what installing needs on each platform. Python 3 can be `python` or `py` on Windows, which also needs Git for Windows. On Linux arm64 the parser's native modules compile during `npm install`, so install build tools first (`sudo apt install build-essential python3`); the full `node:*` Docker images already include them `docs` diff --git a/gemini-extension.json b/gemini-extension.json index 5d36ac9bd..defcedc65 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.3", + "version": "0.1.2", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "contextFileName": "plugins/axiomcode/AGENTS.md", "mcpServers": { diff --git a/package.json b/package.json index 9d61673fd..b38548c41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axiomcode/code-graph", - "version": "0.1.3", + "version": "0.1.2", "description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.", "repository": { "type": "git", @@ -30,11 +30,11 @@ ], "license": "FSL-1.1-Apache-2.0", "optionalDependencies": { - "@axiomcode/engine-darwin-arm64": "0.1.3", - "@axiomcode/engine-darwin-x64": "0.1.3", - "@axiomcode/engine-linux-x64": "0.1.3", - "@axiomcode/engine-linux-arm64": "0.1.3", - "@axiomcode/engine-win32-x64": "0.1.3" + "@axiomcode/engine-darwin-arm64": "0.1.2", + "@axiomcode/engine-darwin-x64": "0.1.2", + "@axiomcode/engine-linux-x64": "0.1.2", + "@axiomcode/engine-linux-arm64": "0.1.2", + "@axiomcode/engine-win32-x64": "0.1.2" }, "bin": { "axiomcode": "bin/axiomcode.js" diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json index 38e818dc4..28f13b63e 100644 --- a/plugins/axiomcode/.claude-plugin/plugin.json +++ b/plugins/axiomcode/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "axiomcode", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", - "version": "0.1.3", + "version": "0.1.2", "author": { "name": "AxiomCode" } diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json index c16a6f50b..f4d18b779 100644 --- a/plugins/axiomcode/.codex-plugin/plugin.json +++ b/plugins/axiomcode/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.3", + "version": "0.1.2", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json index 743e1acf0..a8f886ec1 100644 --- a/plugins/axiomcode/.cursor-plugin/plugin.json +++ b/plugins/axiomcode/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.3", + "version": "0.1.2", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.", "author": { "name": "AxiomCode" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", From 35ff082672ace704de4bec64281a0cbcc169a1d5 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:20:12 -0700 Subject: [PATCH 2/4] remove CHANGELOG.md Release notes live on each GitHub release; the file is not shipped to main. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- CHANGELOG.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 02ad59a40..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -# Changelog - -Every release of `@axiomcode/code-graph` and its `@axiomcode/engine-` packages. The same notes are on each -[GitHub release](https://github.com/AxiomCodeAI/axiomcodegraph/releases). - -## Unreleased - -### Documentation -- #1372 README: what installing needs on each platform. Python 3 can be `python` or `py` on Windows, which also needs Git for Windows. On Linux arm64 the parser's native modules compile during `npm install`, so install build tools first (`sudo apt install build-essential python3`); the full `node:*` Docker images already include them `docs` - -## 0.1.2 — 2026-09-26 - -### Improvements -- #1360 targets: one spelling works in every language. `util.square`, `src.util.square` and `src/util#square` all name the same declaration in `impact`, `path` and `context`; spellings that worked before still work, and a name that fits two declarations lists both instead of guessing `cli` -- #1352 cli: `axiomcode --version` prints the installed version `cli` - -### Bugs -- #1363 windows: `impact`, `test-impact` and `changed --impact` crashed for every user without admin rights (WinError 1314) `windows` -- #1332 windows: a `git.exe`, `python.exe` or `py.exe` in the working directory was run instead of the real one, which could hang the CLI `windows` -- #1369 windows: `axiomcode path '*' …` received the directory's file names instead of `*` `windows` -- #1364 windows: `impact` could not find its query binaries in plugin installs, a console window opened on every background refresh, hooks crashed on files on another drive, WSL's bash could be picked instead of Git Bash, and library paths with spaces were split `windows` -- #1359 macOS: on Apple Silicon Macs whose `python3` is an Intel build (such as Anaconda), `impact` failed unless Soufflé was installed `macos` -- #1361 javascript: `impact` on a class imported with `require` refused it as "declared as more than one kind" `javascript` - -### Build and release -- #1366 every release is tested on all five platforms (Linux x64/arm64, macOS arm64/x64, Windows x64) with every CLI verb before it is tagged `ci` -- #1350 engines are built once per release, and the exact build that passed the tests is the one published `ci` -- #1365 releases and tags are published by axiomcode-bot `ci` - -## 0.1.1 — 2026-09-25 - -### Improvements -- #1336 graph: labels scale with zoom and never overlap, so a small repository opens as cleanly as a large one `graph` - -### Bugs -- #1329 index: exits with code 127 on a clean machine because it needs the sqlite3 command-line program `index` -- #1330 impact and path need Soufflé on the user's machine: the query programs are not shipped compiled `engine` -- #1331 windows: every CLI verb and hook calls python3, which a python.org install does not provide `windows` -- #1340 windows: the index stores paths with backslashes, so context finds no scope, and every path lookup depends on the separator `windows` -- #1341 path --every: the route enumeration grows without bound on a cyclic subgraph and exhausts memory `path` -- #1345 context: a repository with its files at the root has no scope, and the refusal offers an empty list `context` From 76aabddeb2df232bbd9a0160f02ac92d84aab075 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:25:46 -0700 Subject: [PATCH 3/4] README: back to the 0.1.2 text until 0.1.3 is released The installation notes from #1372 change the README, which ships in the npm package, so the version gate needs a bump; bumping pins engine packages that are not published yet. They return with the 0.1.3 release. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8522bc8c8..51cb2e405 100644 --- a/README.md +++ b/README.md @@ -125,18 +125,8 @@ AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) pa graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the agent-facing frontend: a skill, seven MCP tools, and hooks. Install the engine first. -Requirements: **Node ≥ 22.5** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also -[Git for Windows](https://git-scm.com/download/win): the CLI runs under its bash. The engine ships as a prebuilt -binary for macOS (Apple Silicon and Intel), Linux (x64 and arm64) and Windows x64, and `npm install` takes the one for -your platform. No Soufflé and no compiler are needed, with one exception: - -> **Linux arm64** (Graviton or Ampere servers, Raspberry Pi, `node:*-slim` or Alpine containers on an Apple Silicon -> Mac): the parser's native modules compile during `npm install`, so install build tools first, e.g. -> `sudo apt install build-essential python3`. The full `node:*` Docker images already include them. - -Before every release, the exact packages that ship are installed without Soufflé and run end to end (every CLI verb, -five languages) on macOS arm64 and x64, Linux x64 and arm64, and Windows x64. Check an install with -`axiomcode --version`. +Requirements: **Node ≥ 22.5** and **`python3`**. The engine ships as a prebuilt binary and `npm install` +takes the one for your platform; there is nothing else to install. ### Installation From be090ec4c1931138658b1f55ecbabc6b3c2c8b50 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:26:27 -0700 Subject: [PATCH 4/4] python: a script with a python shebang and no .py extension is indexed (#1376) The parser collected only .py and .pyi files, so an executable script named without an extension contributed no symbols and no edges: an edit to it named no declaration, and calls from it into the project's modules were missing from their callers. A file with no extension whose first line is a python shebang is now analysed as a module. The plugin watches and counts such scripts, reads them as Python in changed and impact, and no longer lists them among the files the index does not read as source. Parser version 0.1.1. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- parser/package.json | 2 +- .../python/python-project-analyzer.ts | 26 +++++++++++++++++-- .../skills/axiomcode/scripts/ax_fresh.py | 23 ++++++++++++++-- .../skills/axiomcode/scripts/axiomcode-build | 2 +- .../axiomcode/scripts/axiomcode-changed | 5 ++-- .../skills/axiomcode/scripts/axiomcode-impact | 7 ++++- tests/cases/python/shebang-script/case.json | 14 ++++++++++ tests/cases/python/shebang-script/deploy | 2 ++ tests/cases/python/shebang-script/make-report | 10 +++++++ tests/cases/python/shebang-script/new.txt | 10 +++++++ tests/cases/python/shebang-script/old.txt | 10 +++++++ tests/cases/python/shebang-script/pricing.py | 2 ++ 12 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 tests/cases/python/shebang-script/case.json create mode 100755 tests/cases/python/shebang-script/deploy create mode 100755 tests/cases/python/shebang-script/make-report create mode 100644 tests/cases/python/shebang-script/new.txt create mode 100755 tests/cases/python/shebang-script/old.txt create mode 100644 tests/cases/python/shebang-script/pricing.py diff --git a/parser/package.json b/parser/package.json index fe7527c54..6c9395caa 100644 --- a/parser/package.json +++ b/parser/package.json @@ -1,6 +1,6 @@ { "name": "@axiomcode/parser", - "version": "0.1.0", + "version": "0.1.1", "description": "AxiomCode Parser — compiles source code and build configuration into a relational intermediate representation.", "main": "dist/extract.js", "types": "dist/extract.d.ts", diff --git a/parser/src/workflows/python/python-project-analyzer.ts b/parser/src/workflows/python/python-project-analyzer.ts index 1669761b5..2925a86b9 100644 --- a/parser/src/workflows/python/python-project-analyzer.ts +++ b/parser/src/workflows/python/python-project-analyzer.ts @@ -154,6 +154,28 @@ const CHUNK_SIZE = 50_000; * deferred set for the second freeze, so the rejection detail lives in the * skipped-files CSV until it is unfrozen. */ + +/** + * A `.py`/`.pyi` file, or an executable script with no extension whose first + * line is a python shebang (`#!/usr/bin/env python3`): the interpreter runs it + * as a module, so it is analysed as one (#1376). + */ +const PYTHON_SHEBANG = /^#![^\n]*\bpython[0-9.]*\b/; +export function isPythonSourceFile(full: string, name: string): boolean { + if (name.endsWith('.py') || name.endsWith('.pyi')) return true; + if (name.includes('.')) return false; + let fd: number | undefined; + try { + fd = fs.openSync(full, 'r'); + const head = Buffer.alloc(128); + const n = fs.readSync(fd, head, 0, head.length, 0); + return PYTHON_SHEBANG.test(head.subarray(0, n).toString('utf-8')); + } catch { + return false; + } finally { + if (fd !== undefined) fs.closeSync(fd); + } +} export class PythonProjectAnalyzer { private extractor: PythonFactExtractor; private resolutionLinker: PythonResolutionLinker; @@ -568,7 +590,7 @@ export class PythonProjectAnalyzer { found.push(...(await this.collectPythonFiles(full, excludes, false))); continue; } - if (entry.name.endsWith('.py') || entry.name.endsWith('.pyi')) { + if (entry.isFile() && isPythonSourceFile(full, entry.name)) { found.push(full); } } @@ -603,7 +625,7 @@ export class PythonProjectAnalyzer { found.push(...(await this.collectPythonFiles(full, excludes, false))); continue; } - if (entry.name.endsWith('.py') || entry.name.endsWith('.pyi')) { + if (entry.isFile() && isPythonSourceFile(full, entry.name)) { found.push(full); } } diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py index 57636c7c5..294bf75fc 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py @@ -26,7 +26,7 @@ Environment: AXIOMCODE_NO_REFRESH=1 turns every trigger off; AXIOMCODE_REFRESH_DEBOUNCE (seconds, default 2) is the quiet window; AXIOMCODE_FRESH_WAIT (seconds, default 10) is how long a query verb waits.""" -import errno, hashlib, json, os, subprocess, sys, time +import re, errno, hashlib, json, os, subprocess, sys, time H = os.path.dirname(os.path.abspath(__file__)) @@ -60,12 +60,24 @@ def out_dir(repo): return os.path.join(repo, '.axiomcode', 'out') def table_path(repo): return os.path.join(out_dir(repo), 'files.json') def state_path(repo): return os.path.join(repo, '.axiomcode', 'refresh.json') +PY_SHEBANG = re.compile(r'#![^\n]*\bpython[0-9.]*\b') + +def python_script(p, name=None): + """an executable Python script with no extension: its first line is a python shebang. The parser analyses it as a + module (#1376), so it is watched and counted like a .py file""" + if '.' in (name or os.path.basename(p)): return False + try: + with open(p, 'rb') as fh: head = fh.read(128) + except OSError: return False + return bool(PY_SHEBANG.match(head.decode('utf-8', 'replace'))) + def watched(root, lang): exts, names, prune = EXT.get(lang, ()), NAMES.get(lang, ()), PRUNE.get(lang, PRUNE_ALL) for d, subdirs, files in os.walk(root): subdirs[:] = [s for s in subdirs if s not in prune] for f in files: - if f.endswith(exts) or f in names or (lang == 'java' and os.path.basename(d) == 'services' and 'META-INF' in d): + if f.endswith(exts) or f in names or (lang == 'java' and os.path.basename(d) == 'services' and 'META-INF' in d) \ + or (lang == 'python' and python_script(os.path.join(d, f), f)): yield os.path.join(d, f) def digest(p): @@ -337,6 +349,13 @@ def main(argv): lib = os.environ.get('AXIOMCODE_LIBRARY', '') json.dump(dict(lang=lang, src=src_arg.strip('/'), src_arg=src_arg, library=lib, built=time.time(), files=snapshot(repo, lang, os.path.join(repo, src_arg))), sys.stdout); return 0 + if cmd == 'pyscripts': + # how many extensionless python scripts are under repo: added to the build's .py count when it picks a language + n = 0 + for d, subdirs, files in os.walk(repo): + subdirs[:] = [x for x in subdirs if x not in PRUNE_ALL] + n += sum(1 for f in files if python_script(os.path.join(d, f), f)) + print(n); return 0 if cmd == 'uptodate': # exit 0 when the recorded table was built with this language / --src / --library and no file differs lang, src_arg, lib = argv[3], argv[4] if len(argv) > 4 else '', argv[5] if len(argv) > 5 else '' diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build index 4724ce140..bb7fe597a 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build @@ -49,7 +49,7 @@ ENGINE="$AXIOMCODE_ENGINE" exit 1; } # no `head` in this pipeline: under pipefail a closed pipe kills find and, with set -e, the script — silently count(){ find "$SRC" \( -path '*/node_modules' -o -path '*/.axiomcode' -o -path '*/.git' -o -path '*/.venv' -o -path '*/venv' -o -path '*/site-packages' -o -path '*/dist' -o -path '*/build' \) -prune -o -type f \( "$@" \) -print 2>/dev/null | wc -l | tr -d ' '; } -py=$(count -name '*.py'); ja=$(count -name '*.java'); ts=$(count -name '*.ts' -o -name '*.tsx'); js=$(count -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.jsx'); cs=$(count -name '*.cs') +py=$(( $(count -name '*.py') + $(python3 "$H/ax_fresh.py" pyscripts "$SRC" 2>/dev/null || echo 0) )); ja=$(count -name '*.java'); ts=$(count -name '*.ts' -o -name '*.tsx'); js=$(count -name '*.js' -o -name '*.mjs' -o -name '*.cjs' -o -name '*.jsx'); cs=$(count -name '*.cs') LANG_ARG="${AXIOMCODE_LANG:-}" if [ -z "$LANG_ARG" ]; then LANG_ARG=java; best=$ja diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed index 0055d86c0..423e16150 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed @@ -196,6 +196,7 @@ class Changed: # ── one file: the declarations touched and how ──────────────────────────────────────────────────────────── def file_changes(self, rel, old, new): OL, NL = old.split('\n'), new.split('\n') + is_py = rel.endswith(('.py', '.pyi')) or bool(re.match(r'#![^\n]*\bpython[0-9.]*\b', new or old)) # a shebang script too (#1376) sm = difflib.SequenceMatcher(None, OL, NL, autojunk=False) old_changed = set(); new_of = {}; added = []; removed_lines = set(); replaced = [] # a blank line, a comment line, a brace on its own: not a change to a declaration @@ -275,7 +276,7 @@ class Changed: if kind == 'signature': # compared with comments removed: `f( /* note */ a)` is `f(a)`. Strings stay, so an edited route in a # decoration is still a header change - uncomment = lambda lines: re.sub(r'\s+', ' ', strip_code('\n'.join(lines), strings=False, hash_comments=rel.endswith('.py'))).strip() + uncomment = lambda lines: re.sub(r'\s+', ' ', strip_code('\n'.join(lines), strings=False, hash_comments=is_py)).strip() oh_raw = ' '.join(x.strip() for x in OL[a - 1:self.header_end(OL, a)]); oh = uncomment(OL[a - 1:self.header_end(OL, a)]) na = new_of.get(a) or (new_of.get(max((x for x in new_of if x < a), default=0), 0) + 1) nh = nh_raw = '' @@ -352,7 +353,7 @@ class Changed: names = []; hdrs = {} # the type's body level in the NEW text: brace depth just after its header (indentation for Python); a declaration # the insertion introduces sits exactly there — a local inside a new method's body sits deeper - py = rel.endswith('.py'); tline = new_of.get(t[0]) if t else None + py = is_py; tline = new_of.get(t[0]) if t else None if t and not tline: # the type's header line moved: find it in the new text by name tline = next((j for j in range(1, len(NL) + 1) if re.search(rf'\b(class|interface|enum|record|trait|struct)\s+{re.escape(t[5])}\b', NL[j - 1])), None) if tline: body_depth = ndepth[self.header_end(NL, tline)] if not py else len(NL[tline - 1]) - len(NL[tline - 1].lstrip()) + 4 diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 8f5c9f22e..4c6def0a0 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -198,7 +198,7 @@ class Impact: """the source with comments and string literals blanked (same length, same line breaks): a name in a javadoc, a docstring or a string is not a use. Block /* */, line // and # (Python), '…' "…" `…` and triple-quoted strings.""" if ('code', file) in self.src: return self.src[('code', file)] - t = '\n'.join(self.lines(file)); out = []; i = 0; n = len(t); py = file.endswith('.py') + t = '\n'.join(self.lines(file)); out = []; i = 0; n = len(t); py = file.endswith(('.py', '.pyi')) or bool(re.match(r'#![^\n]*\bpython[0-9.]*\b', t)) triple = py or file.endswith(('.java', '.kt', '.scala', '.groovy')) # a Java text block is """ too js = file.endswith(('.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx')) prev = '' # the last code character seen @@ -593,6 +593,9 @@ class Impact: out = set() for f in files: ext = os.path.splitext(f)[1].lower() + if not ext: # a python shebang script (#1376) + try: ext = '.py' if re.match(r'#![^\n]*\bpython[0-9.]*\b', (self.lines(f) or [''])[0]) else '' + except Exception: ext = '' rx = self.IMPORT_RE.get(ext) if not rx: continue try: text = '\n'.join(self.lines(f)) @@ -625,12 +628,14 @@ class Impact: a services file, a properties file, a build script, a spec) breaks at run time and the compiler is silent.""" if hasattr(self, '_nsf'): return self._nsf out = [] + indexed = {sy['file'] for sy in self.g.sym.values() if sy.get('file')} # a shebang script is source too (#1376) for root, dirs, files in os.walk(self.g.repo): dirs[:] = [d for d in dirs if d not in self.SKIP_DIR and not d.startswith('.axiomcode')] for fn in files: ext = os.path.splitext(fn)[1].lower() if ext in self.SOURCE_EXT or ext in self.PROSE_EXT: continue fp = os.path.join(root, fn) + if os.path.relpath(fp, self.g.repo).replace(os.sep, '/') in indexed: continue try: if os.path.getsize(fp) > 512 * 1024: continue with open(fp, 'rb') as fh: head = fh.read(2048) diff --git a/tests/cases/python/shebang-script/case.json b/tests/cases/python/shebang-script/case.json new file mode 100644 index 000000000..885b16155 --- /dev/null +++ b/tests/cases/python/shebang-script/case.json @@ -0,0 +1,14 @@ +{"lang": "python", "src": ".", + "checks": [ + {"why": "a script with a python shebang and no .py extension is a module: the function it calls has it as a caller, and it is not listed among the files the index does not read as source", + "run": ["impact", "price"], + "want": ["report"], + "avoid": ["[text] make-report"]}, + {"why": "a shell script that only runs python3 is not read as Python: its mention of the function stays a text match, not a caller", + "run": ["impact", "price"], + "want": ["[text] deploy:2"], + "avoid": ["deploy."]}, + {"why": "an edit inside a shebang script names the function it changed, so test-impact has a declaration to start from", + "run": ["changed", "{repo}", "--old", "{repo}/old.txt", "--new", "{repo}/new.txt", "--file", "make-report"], + "want": ["body", "report"], + "avoid": ["no declarations known here", "no changed declaration"]}]} diff --git a/tests/cases/python/shebang-script/deploy b/tests/cases/python/shebang-script/deploy new file mode 100755 index 000000000..da5df3553 --- /dev/null +++ b/tests/cases/python/shebang-script/deploy @@ -0,0 +1,2 @@ +#!/bin/sh +python3 -c 'from pricing import price; print(price(1, 1))' diff --git a/tests/cases/python/shebang-script/make-report b/tests/cases/python/shebang-script/make-report new file mode 100755 index 000000000..c95b99a70 --- /dev/null +++ b/tests/cases/python/shebang-script/make-report @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +from pricing import price + + +def report(): + return price(3, 5) + + +if __name__ == '__main__': + print(report()) diff --git a/tests/cases/python/shebang-script/new.txt b/tests/cases/python/shebang-script/new.txt new file mode 100644 index 000000000..7a4550003 --- /dev/null +++ b/tests/cases/python/shebang-script/new.txt @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +from pricing import price + + +def report(): + return price(4, 5) + + +if __name__ == '__main__': + print(report()) diff --git a/tests/cases/python/shebang-script/old.txt b/tests/cases/python/shebang-script/old.txt new file mode 100755 index 000000000..c95b99a70 --- /dev/null +++ b/tests/cases/python/shebang-script/old.txt @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +from pricing import price + + +def report(): + return price(3, 5) + + +if __name__ == '__main__': + print(report()) diff --git a/tests/cases/python/shebang-script/pricing.py b/tests/cases/python/shebang-script/pricing.py new file mode 100644 index 000000000..53f2f8440 --- /dev/null +++ b/tests/cases/python/shebang-script/pricing.py @@ -0,0 +1,2 @@ +def price(qty, unit): + return qty * unit