ast-search-python 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -15,8 +15,10 @@ ast-search <query> --plugin ast-search-python [--dir <path>] [--format text|json
15
15
  | `--plugin` | `-p` | — | Must be `ast-search-python` to activate Python support |
16
16
  | `--dir` | `-d` | `cwd` | Root directory to search |
17
17
  | `--format` | `-f` | `text` | Output format: `text`, `json`, `files`, or `count` |
18
+ | `--exclude` | `-x` | none | Glob pattern(s) to exclude from search (repeatable) |
18
19
  | `--lang` | `-l` | all | Pass `python` to restrict to Python files only |
19
20
  | `--context` | `-C` | `0` | Show N lines of context around each match (like `grep -C`) |
21
+ | `--show-ast` | — | off | Print the tree-sitter AST subtree of each matched node below the match line |
20
22
  | `--ast` | — | off | Print Python AST for a snippet or `--file`; requires `--lang python` |
21
23
 
22
24
  **Exit codes:** `0` = matches found · `1` = no matches · `2` = error (invalid selector, etc.)
@@ -83,13 +85,15 @@ Text output appends captures after the source line:
83
85
  src/app.py:5:0: logging.info("user logged in") | fn=logging.info msg="user logged in" call=logging.info("user logged in")
84
86
  ```
85
87
 
86
- JSON output includes a `captures` field when captures are present:
88
+ JSON output includes `start`/`end` byte offsets and `source_full` (for multi-line matches) in addition to `captures`:
87
89
 
88
90
  ```json
89
91
  {
90
92
  "file": "src/app.py",
91
93
  "line": 5,
92
94
  "col": 0,
95
+ "start": 87,
96
+ "end": 116,
93
97
  "source": "logging.info(\"user logged in\")",
94
98
  "captures": {
95
99
  "fn": "logging.info",
@@ -99,6 +103,11 @@ JSON output includes a `captures` field when captures are present:
99
103
  }
100
104
  ```
101
105
 
106
+ - `start` / `end`: **byte offsets** from the start of the file (not UTF-16 character offsets as in the JS backend). For ASCII-only source these are equivalent; for non-ASCII source (e.g. Unicode identifiers or string literals), byte offsets and character offsets diverge — account for this when slicing file contents.
107
+ - `source_full`: full text of the matched node. Only present when the match spans multiple lines (differs from `source`).
108
+
109
+ ⚠️ **Python `start`/`end` are byte offsets; JS/TS `start`/`end` are UTF-16 character offsets.** When writing tools that consume both, use the `file`/`line`/`col` fields for display and treat `start`/`end` as language-specific.
110
+
102
111
  All captures from a single pattern application are grouped onto one match — `@fn`, `@msg`, and `@call` from the same query match produce one result, not three.
103
112
 
104
113
  ---
@@ -204,6 +213,6 @@ const matches = await searchRepo('fn', './src');
204
213
  - **`async def` functions are typed as `function_definition`** in tree-sitter-python 0.21+. There is no separate `async_function_definition` node. The `fn` shorthand matches both. To match only async, use a predicate query.
205
214
  - **Shorthands are not expanded inside quoted strings.** `'(call function: (identifier) @n (#eq? @n "fn"))'` keeps `"fn"` literal.
206
215
  - **Unparseable files are silently skipped.** Syntax errors in source files do not abort the search.
207
- - **`node_modules` is always excluded**, as are files/directories whose names start with `.`.
216
+ - **`node_modules` is always excluded**, as are files/directories whose names start with `.`. Use `--exclude` / `-x` for additional patterns (e.g. `--exclude '**/*_test.py'`). Patterns match against paths relative to `--dir`.
208
217
  - **Verify the AST structure before writing a query.** Python attribute chains like `self.client.send()` nest deeply — `self` is not the direct `object:` of the outer call; `self.client` is. If a predicate query returns no results, first remove the predicate and confirm the base pattern matches what you expect.
209
218
  - **All captures from one pattern match are grouped on a single result.** A query like `(function_definition name: (identifier) @n) @fn` produces one match per function, with both `@fn` and `@n` in the `captures` field. The anchor (location) is the first non-underscore capture — `@fn` in this case.
@@ -0,0 +1,18 @@
1
+ export interface TSNodeFull {
2
+ type: string;
3
+ isNamed: boolean;
4
+ text: string;
5
+ children: TSNodeFull[];
6
+ startPosition: {
7
+ row: number;
8
+ column: number;
9
+ };
10
+ endPosition: {
11
+ row: number;
12
+ column: number;
13
+ };
14
+ fieldNameForChild(index: number): string | null;
15
+ }
16
+ export declare function printTSNodeText(node: TSNodeFull, out: string[], indent: string, fieldName?: string): void;
17
+ export declare function printMatchTSNode(node: TSNodeFull): string;
18
+ export declare function serializeTSNode(node: TSNodeFull): Record<string, unknown>;
@@ -0,0 +1,51 @@
1
+ export function printTSNodeText(node, out, indent, fieldName) {
2
+ var _a;
3
+ const label = fieldName ? `${fieldName}: ${node.type}` : node.type;
4
+ const namedChildren = node.children.filter((c) => c.isNamed);
5
+ if (namedChildren.length === 0) {
6
+ const text = node.text.replace(/\n/g, "\\n");
7
+ const suffix = text.length <= 60 ? ` [text="${text}"]` : "";
8
+ out.push(`${indent}${label}${suffix}`);
9
+ }
10
+ else {
11
+ out.push(`${indent}${label}`);
12
+ for (let i = 0; i < node.children.length; i++) {
13
+ const child = node.children[i];
14
+ if (!child.isNamed)
15
+ continue;
16
+ const childField = (_a = node.fieldNameForChild(i)) !== null && _a !== void 0 ? _a : undefined;
17
+ printTSNodeText(child, out, indent + " ", childField);
18
+ }
19
+ }
20
+ }
21
+ export function printMatchTSNode(node) {
22
+ const out = [];
23
+ printTSNodeText(node, out, "", undefined);
24
+ return out.join("\n");
25
+ }
26
+ export function serializeTSNode(node) {
27
+ const result = {
28
+ type: node.type,
29
+ isNamed: node.isNamed,
30
+ start: node.startPosition,
31
+ end: node.endPosition,
32
+ };
33
+ const namedChildren = [];
34
+ for (let i = 0; i < node.children.length; i++) {
35
+ const child = node.children[i];
36
+ if (!child.isNamed)
37
+ continue;
38
+ const fieldName = node.fieldNameForChild(i);
39
+ const serialized = serializeTSNode(child);
40
+ if (fieldName)
41
+ serialized.field = fieldName;
42
+ namedChildren.push(serialized);
43
+ }
44
+ if (namedChildren.length > 0) {
45
+ result.children = namedChildren;
46
+ }
47
+ else {
48
+ result.text = node.text;
49
+ }
50
+ return result;
51
+ }
package/build/index.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import type { LanguageBackend, LanguageRegistry, Match } from "ast-search-js/plugin";
2
+ import { printMatchTSNode } from "./ast-print.js";
3
+ export { printMatchTSNode };
2
4
  export declare class PythonLanguageBackend implements LanguageBackend {
3
5
  readonly langId = "python";
4
6
  readonly name = "Python";
5
7
  readonly extensions: Set<string>;
6
8
  parse(source: string, _filePath: string): Promise<unknown>;
7
- query(ast: unknown, selector: string, source: string, filePath: string): Promise<Match[]>;
9
+ query(ast: unknown, selector: string, source: string, filePath: string, options?: {
10
+ showAst?: boolean;
11
+ }): Promise<Match[]>;
8
12
  validateSelector(selector: string): Promise<void>;
9
13
  printAst(ast: unknown, _source: string, format: "text" | "json"): string;
10
14
  }
package/build/index.js CHANGED
@@ -11,53 +11,9 @@ import { createRequire } from "module";
11
11
  import path from "path";
12
12
  import { expandShorthands } from "./shorthands.js";
13
13
  import { runTreeSitterQuery, validateTreeSitterQuery } from "./query.js";
14
+ import { printTSNodeText, printMatchTSNode, serializeTSNode } from "./ast-print.js";
15
+ export { printMatchTSNode };
14
16
  const _require = createRequire(import.meta.url);
15
- function printTSNodeText(node, out, indent, fieldName) {
16
- var _a;
17
- const label = fieldName ? `${fieldName}: ${node.type}` : node.type;
18
- const namedChildren = node.children.filter((c) => c.isNamed);
19
- if (namedChildren.length === 0) {
20
- const text = node.text.replace(/\n/g, "\\n");
21
- const suffix = text.length <= 60 ? ` [text="${text}"]` : "";
22
- out.push(`${indent}${label}${suffix}`);
23
- }
24
- else {
25
- out.push(`${indent}${label}`);
26
- for (let i = 0; i < node.children.length; i++) {
27
- const child = node.children[i];
28
- if (!child.isNamed)
29
- continue;
30
- const childField = (_a = node.fieldNameForChild(i)) !== null && _a !== void 0 ? _a : undefined;
31
- printTSNodeText(child, out, indent + " ", childField);
32
- }
33
- }
34
- }
35
- function serializeTSNode(node) {
36
- const result = {
37
- type: node.type,
38
- isNamed: node.isNamed,
39
- start: node.startPosition,
40
- end: node.endPosition,
41
- };
42
- const namedChildren = [];
43
- for (let i = 0; i < node.children.length; i++) {
44
- const child = node.children[i];
45
- if (!child.isNamed)
46
- continue;
47
- const fieldName = node.fieldNameForChild(i);
48
- const serialized = serializeTSNode(child);
49
- if (fieldName)
50
- serialized.field = fieldName;
51
- namedChildren.push(serialized);
52
- }
53
- if (namedChildren.length > 0) {
54
- result.children = namedChildren;
55
- }
56
- else {
57
- result.text = node.text;
58
- }
59
- return result;
60
- }
61
17
  let _runtimePromise = null;
62
18
  function getRuntime() {
63
19
  return __awaiter(this, void 0, void 0, function* () {
@@ -90,11 +46,12 @@ export class PythonLanguageBackend {
90
46
  return parser.parse(source);
91
47
  });
92
48
  }
93
- query(ast, selector, source, filePath) {
49
+ query(ast, selector, source, filePath, options) {
94
50
  return __awaiter(this, void 0, void 0, function* () {
51
+ var _a;
95
52
  const { language } = yield getRuntime();
96
53
  const expanded = expandShorthands(selector);
97
- return runTreeSitterQuery(ast, expanded, source, filePath, language);
54
+ return runTreeSitterQuery(ast, expanded, source, filePath, language, (_a = options === null || options === void 0 ? void 0 : options.showAst) !== null && _a !== void 0 ? _a : false);
98
55
  });
99
56
  }
100
57
  validateSelector(selector) {
package/build/query.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { Match } from "ast-search-js/plugin";
2
- export declare function runTreeSitterQuery(ast: unknown, pattern: string, source: string, filePath: string, language: unknown): Match[];
2
+ export declare function runTreeSitterQuery(ast: unknown, pattern: string, source: string, filePath: string, language: unknown, showAst?: boolean): Match[];
3
3
  export declare function validateTreeSitterQuery(pattern: string, language: unknown): void;
package/build/query.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { printMatchTSNode } from "./ast-print.js";
1
2
  function assertValidPattern(pattern) {
2
3
  const trimmed = pattern.trim();
3
4
  // A valid tree-sitter pattern must start with "(" (S-expression) or contain
@@ -7,7 +8,7 @@ function assertValidPattern(pattern) {
7
8
  `Use a shorthand (e.g. "fn", "call") or write a full S-expression (e.g. "(function_definition) @fn").`);
8
9
  }
9
10
  }
10
- export function runTreeSitterQuery(ast, pattern, source, filePath, language) {
11
+ export function runTreeSitterQuery(ast, pattern, source, filePath, language, showAst = false) {
11
12
  var _a;
12
13
  assertValidPattern(pattern);
13
14
  const tree = ast;
@@ -40,7 +41,7 @@ export function runTreeSitterQuery(ast, pattern, source, filePath, language) {
40
41
  captureMap[cap.name] = source.slice(cap.node.startIndex, cap.node.endIndex);
41
42
  }
42
43
  }
43
- results.push(Object.assign({ file: filePath, line: anchor.node.startPosition.row + 1, col: anchor.node.startPosition.column, source: firstLine }, (Object.keys(captureMap).length > 0 ? { captures: captureMap } : {})));
44
+ results.push(Object.assign(Object.assign(Object.assign({ file: filePath, line: anchor.node.startPosition.row + 1, col: anchor.node.startPosition.column, start: anchor.node.startIndex, end: anchor.node.endIndex, source: firstLine }, (text !== firstLine ? { source_full: text } : {})), (showAst ? { astSubtree: printMatchTSNode(anchor.node) } : {})), (Object.keys(captureMap).length > 0 ? { captures: captureMap } : {})));
44
45
  }
45
46
  return results;
46
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ast-search-python",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Python language plugin for ast-search",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",