ast-search-python 1.0.6 → 1.1.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 +41 -1
- package/README.md +14 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +55 -0
- package/build/query.js +21 -14
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -71,6 +71,34 @@ Python queries use [tree-sitter](https://tree-sitter.github.io/tree-sitter/) S-e
|
|
|
71
71
|
|
|
72
72
|
Output formats (`text`, `json`, `files`) work identically to the core tool. See [ast-search AGENTS](../../AGENTS.md#output-formats) for details.
|
|
73
73
|
|
|
74
|
+
### Captures
|
|
75
|
+
|
|
76
|
+
Named captures (`@name` in your pattern, excluding `@_`) are included in the `captures` field of each match. The capture key is the name without the `@`.
|
|
77
|
+
|
|
78
|
+
Text output appends captures after the source line:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
src/app.py:5:0: logging.info("user logged in") | fn=logging.info msg="user logged in" call=logging.info("user logged in")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
JSON output includes a `captures` field when captures are present:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"file": "src/app.py",
|
|
89
|
+
"line": 5,
|
|
90
|
+
"col": 0,
|
|
91
|
+
"source": "logging.info(\"user logged in\")",
|
|
92
|
+
"captures": {
|
|
93
|
+
"fn": "logging.info",
|
|
94
|
+
"msg": "\"user logged in\"",
|
|
95
|
+
"call": "logging.info(\"user logged in\")"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
74
102
|
---
|
|
75
103
|
|
|
76
104
|
## Python Refactoring Patterns
|
|
@@ -96,6 +124,18 @@ ast-search 'from' --plugin ast-search-python
|
|
|
96
124
|
ast-search '(call function: (identifier) @n (#eq? @n "my_func")) @c' --plugin ast-search-python
|
|
97
125
|
```
|
|
98
126
|
|
|
127
|
+
### Find calls to any logging method and capture the method name and string argument
|
|
128
|
+
```bash
|
|
129
|
+
ast-search '(call function: [(identifier)(attribute)] @fn (#match? @fn "^(log|info|warn|error|debug)") arguments: (argument_list (string) @msg)) @call' --plugin ast-search-python
|
|
130
|
+
# output: ... | fn=logging.info msg="user logged in" call=logging.info("user logged in")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Find functions whose name matches a regex
|
|
134
|
+
```bash
|
|
135
|
+
ast-search '(function_definition name: (identifier) @name (#match? @name "^handle_")) @fn' --plugin ast-search-python
|
|
136
|
+
# output: ... | name=handle_request fn=def handle_request(...):
|
|
137
|
+
```
|
|
138
|
+
|
|
99
139
|
### Find all decorators
|
|
100
140
|
```bash
|
|
101
141
|
ast-search 'decorator' --plugin ast-search-python
|
|
@@ -164,4 +204,4 @@ const matches = await searchRepo('fn', './src');
|
|
|
164
204
|
- **Unparseable files are silently skipped.** Syntax errors in source files do not abort the search.
|
|
165
205
|
- **`node_modules` is always excluded**, as are files/directories whose names start with `.`.
|
|
166
206
|
- **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.
|
|
167
|
-
-
|
|
207
|
+
- **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.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ast-search-python
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/ast-search-python)
|
|
4
|
+
|
|
3
5
|
Python language plugin for [ast-search-js](../ast-search-js/README.md). Adds `.py` / `.pyw` file support using [tree-sitter](https://tree-sitter.github.io/tree-sitter/) S-expression queries.
|
|
4
6
|
|
|
5
7
|
## Table of Contents
|
|
@@ -65,11 +67,22 @@ ast-search '(class_definition) @cls' --plugin ast-search-python
|
|
|
65
67
|
# Find all calls to a specific function by name
|
|
66
68
|
ast-search '(call function: (identifier) @n (#eq? @n "my_func")) @c' --plugin ast-search-python
|
|
67
69
|
|
|
70
|
+
# Find calls to any function matching a regex (using #match? predicate)
|
|
71
|
+
ast-search '(call function: (identifier) @n (#match? @n "^(get|post|put|delete)_")) @c' --plugin ast-search-python
|
|
72
|
+
|
|
68
73
|
# Restrict to only Python files in a mixed-language repo
|
|
69
74
|
ast-search 'fn' --lang python --plugin ast-search-python
|
|
70
75
|
```
|
|
71
76
|
|
|
72
|
-
Raw S-expression queries must include at least one `@capture_name` — tree-sitter
|
|
77
|
+
Raw S-expression queries must include at least one `@capture_name` — tree-sitter requires it to return results. Shorthands include `@_` automatically.
|
|
78
|
+
|
|
79
|
+
Named captures (`@name`, excluding `@_`) appear in the `captures` field of each match. All captures from a single pattern application are grouped on one result. In text output they appear after ` | `; in JSON they're in a `captures` object.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Find logging calls — capture the method name and string argument
|
|
83
|
+
ast-search '(call function: [(identifier)(attribute)] @fn (#match? @fn "^(log|info|warn|error)") arguments: (argument_list (string) @msg)) @call' --plugin ast-search-python
|
|
84
|
+
# text output: src/app.py:5:0: logging.info("msg") | fn=logging.info msg="msg" call=logging.info("msg")
|
|
85
|
+
```
|
|
73
86
|
|
|
74
87
|
### Shorthands
|
|
75
88
|
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare class PythonLanguageBackend implements LanguageBackend {
|
|
|
6
6
|
parse(source: string, _filePath: string): Promise<unknown>;
|
|
7
7
|
query(ast: unknown, selector: string, source: string, filePath: string): Promise<Match[]>;
|
|
8
8
|
validateSelector(selector: string): Promise<void>;
|
|
9
|
+
printAst(ast: unknown, _source: string, format: "text" | "json"): string;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* Register the Python backend with an ast-search LanguageRegistry.
|
package/build/index.js
CHANGED
|
@@ -12,6 +12,52 @@ import path from "path";
|
|
|
12
12
|
import { expandShorthands } from "./shorthands.js";
|
|
13
13
|
import { runTreeSitterQuery, validateTreeSitterQuery } from "./query.js";
|
|
14
14
|
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
|
+
}
|
|
15
61
|
let _runtimePromise = null;
|
|
16
62
|
function getRuntime() {
|
|
17
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -57,6 +103,15 @@ export class PythonLanguageBackend {
|
|
|
57
103
|
validateTreeSitterQuery(expandShorthands(selector), language);
|
|
58
104
|
});
|
|
59
105
|
}
|
|
106
|
+
printAst(ast, _source, format) {
|
|
107
|
+
const tree = ast;
|
|
108
|
+
if (format === "json") {
|
|
109
|
+
return JSON.stringify(serializeTSNode(tree.rootNode), null, 2);
|
|
110
|
+
}
|
|
111
|
+
const out = [];
|
|
112
|
+
printTSNodeText(tree.rootNode, out, "", undefined);
|
|
113
|
+
return out.join("\n");
|
|
114
|
+
}
|
|
60
115
|
}
|
|
61
116
|
/**
|
|
62
117
|
* Register the Python backend with an ast-search LanguageRegistry.
|
package/build/query.js
CHANGED
|
@@ -8,32 +8,39 @@ function assertValidPattern(pattern) {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
export function runTreeSitterQuery(ast, pattern, source, filePath, language) {
|
|
11
|
+
var _a;
|
|
11
12
|
assertValidPattern(pattern);
|
|
12
13
|
const tree = ast;
|
|
13
|
-
//
|
|
14
|
+
// matches() only returns nodes that have a capture name (@something).
|
|
14
15
|
// If the user wrote a bare S-expression like (function_definition), add @_
|
|
15
16
|
// so results are returned.
|
|
16
17
|
const queryPattern = pattern.includes("@") ? pattern : `${pattern} @_`;
|
|
17
18
|
const q = language.query(queryPattern);
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
// multiple capture names match it, so deduplicate by position instead of identity.
|
|
19
|
+
// Use matches() instead of captures() so all captures from one pattern
|
|
20
|
+
// application are grouped together — enabling multi-node capture output.
|
|
21
21
|
const seen = new Set();
|
|
22
22
|
const results = [];
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
for (const match of q.matches(tree.rootNode)) {
|
|
24
|
+
// Anchor on the first non-underscore capture (the primary match location).
|
|
25
|
+
// @_ is the auto-appended anonymous marker; user captures like @fn, @msg
|
|
26
|
+
// are the meaningful ones and also serve as the anchor.
|
|
27
|
+
const anchor = (_a = match.captures.find((c) => !c.name.startsWith("_"))) !== null && _a !== void 0 ? _a : match.captures[0];
|
|
28
|
+
if (!anchor)
|
|
29
|
+
continue;
|
|
30
|
+
const key = `${anchor.node.startIndex}:${anchor.node.endIndex}`;
|
|
26
31
|
if (seen.has(key))
|
|
27
32
|
continue;
|
|
28
33
|
seen.add(key);
|
|
29
|
-
const text = source.slice(node.startIndex, node.endIndex);
|
|
34
|
+
const text = source.slice(anchor.node.startIndex, anchor.node.endIndex);
|
|
30
35
|
const firstLine = text.split("\n")[0].trimEnd();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
// Collect all named captures except the anonymous @_ marker.
|
|
37
|
+
const captureMap = {};
|
|
38
|
+
for (const cap of match.captures) {
|
|
39
|
+
if (!cap.name.startsWith("_")) {
|
|
40
|
+
captureMap[cap.name] = source.slice(cap.node.startIndex, cap.node.endIndex);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
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 } : {})));
|
|
37
44
|
}
|
|
38
45
|
return results;
|
|
39
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ast-search-python",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Python language plugin for ast-search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"prepublishOnly": "tsc"
|
|
17
17
|
},
|
|
18
18
|
"author": "shiplet",
|
|
19
|
-
"license": "
|
|
19
|
+
"license": "MIT",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
22
|
"url": "https://github.com/willey-shiplet/ast-search.git",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"build/*.js",
|
|
27
27
|
"build/*.d.ts",
|
|
28
28
|
"README.md",
|
|
29
|
-
"AGENTS.md"
|
|
29
|
+
"AGENTS.md",
|
|
30
|
+
"LICENSE"
|
|
30
31
|
],
|
|
31
32
|
"publishConfig": {
|
|
32
33
|
"access": "public",
|
|
@@ -40,12 +41,11 @@
|
|
|
40
41
|
"@jest/globals": "^29.7.0",
|
|
41
42
|
"@types/jest": "^29.5.12",
|
|
42
43
|
"@types/node": "^20.12.7",
|
|
43
|
-
"ast-search-js": "1.0.2",
|
|
44
44
|
"jest": "^29.7.0",
|
|
45
45
|
"ts-jest": "^29.1.2",
|
|
46
46
|
"typescript": "^5.4.5"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"ast-search-js": "1.0.
|
|
49
|
+
"ast-search-js": "^1.0.0"
|
|
50
50
|
}
|
|
51
51
|
}
|