ast-search-python 0.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.
@@ -0,0 +1,14 @@
1
+ import type { LanguageBackend, LanguageRegistry, Match } from "ast-search/plugin";
2
+ export declare class PythonLanguageBackend implements LanguageBackend {
3
+ readonly langId = "python";
4
+ readonly name = "Python";
5
+ readonly extensions: Set<string>;
6
+ parse(source: string, _filePath: string): unknown;
7
+ query(ast: unknown, selector: string, source: string, filePath: string): Match[];
8
+ validateSelector(selector: string): void;
9
+ }
10
+ /**
11
+ * Register the Python backend with an ast-search LanguageRegistry.
12
+ * Called by ast-search core when --plugin ast-search-python is passed.
13
+ */
14
+ export declare function register(registry: LanguageRegistry): void;
package/build/index.js ADDED
@@ -0,0 +1,53 @@
1
+ import { createRequire } from "module";
2
+ import { expandShorthands } from "./shorthands.js";
3
+ import { runTreeSitterQuery, validateTreeSitterQuery } from "./query.js";
4
+ // tree-sitter is a CommonJS module; use createRequire for ESM compat.
5
+ const _require = createRequire(import.meta.url);
6
+ // Lazy-initialize the parser so the native addon is only loaded when
7
+ // a Python file is actually parsed (not at module import time).
8
+ let _parser;
9
+ let _language;
10
+ let _QueryClass;
11
+ function getRuntime() {
12
+ if (!_parser) {
13
+ const Parser = _require("tree-sitter");
14
+ const pythonModule = _require("tree-sitter-python");
15
+ _language = pythonModule.language;
16
+ _QueryClass = Parser.Query;
17
+ const p = new Parser();
18
+ p.setLanguage(pythonModule);
19
+ _parser = p;
20
+ }
21
+ return {
22
+ parser: _parser,
23
+ language: _language,
24
+ QueryClass: _QueryClass,
25
+ };
26
+ }
27
+ export class PythonLanguageBackend {
28
+ constructor() {
29
+ this.langId = "python";
30
+ this.name = "Python";
31
+ this.extensions = new Set([".py", ".pyw"]);
32
+ }
33
+ parse(source, _filePath) {
34
+ const { parser } = getRuntime();
35
+ return parser.parse(source);
36
+ }
37
+ query(ast, selector, source, filePath) {
38
+ const { language, QueryClass } = getRuntime();
39
+ const expanded = expandShorthands(selector);
40
+ return runTreeSitterQuery(ast, expanded, source, filePath, language, QueryClass);
41
+ }
42
+ validateSelector(selector) {
43
+ const { language, QueryClass } = getRuntime();
44
+ validateTreeSitterQuery(expandShorthands(selector), language, QueryClass);
45
+ }
46
+ }
47
+ /**
48
+ * Register the Python backend with an ast-search LanguageRegistry.
49
+ * Called by ast-search core when --plugin ast-search-python is passed.
50
+ */
51
+ export function register(registry) {
52
+ registry.register(new PythonLanguageBackend());
53
+ }
@@ -0,0 +1,23 @@
1
+ import type { Match } from "ast-search/plugin";
2
+ interface TSNode {
3
+ startPosition: {
4
+ row: number;
5
+ column: number;
6
+ };
7
+ startIndex: number;
8
+ endIndex: number;
9
+ type: string;
10
+ }
11
+ interface TSCapture {
12
+ node: TSNode;
13
+ name: string;
14
+ }
15
+ interface TSQueryConstructor {
16
+ new (language: unknown, pattern: string): TSQuery;
17
+ }
18
+ interface TSQuery {
19
+ captures(node: TSNode): TSCapture[];
20
+ }
21
+ export declare function runTreeSitterQuery(ast: unknown, pattern: string, source: string, filePath: string, language: unknown, QueryClass: TSQueryConstructor): Match[];
22
+ export declare function validateTreeSitterQuery(pattern: string, language: unknown, QueryClass: TSQueryConstructor): void;
23
+ export {};
package/build/query.js ADDED
@@ -0,0 +1,41 @@
1
+ function assertValidPattern(pattern) {
2
+ const trimmed = pattern.trim();
3
+ // A valid tree-sitter pattern must start with "(" (S-expression) or contain
4
+ // "@" (capture). Bare words crash tree-sitter with a native SIGSEGV.
5
+ if (!trimmed.startsWith("(") && !trimmed.includes("@")) {
6
+ throw new Error(`Invalid tree-sitter query "${trimmed}": must start with "(" or include a "@capture". ` +
7
+ `Use a shorthand (e.g. "fn", "call") or write a full S-expression (e.g. "(function_definition) @fn").`);
8
+ }
9
+ }
10
+ export function runTreeSitterQuery(ast, pattern, source, filePath, language, QueryClass) {
11
+ assertValidPattern(pattern);
12
+ const tree = ast;
13
+ const q = new QueryClass(language, pattern);
14
+ const captures = q.captures(tree.rootNode);
15
+ const seen = new Set();
16
+ const results = [];
17
+ for (const capture of captures) {
18
+ const node = capture.node;
19
+ if (seen.has(node))
20
+ continue;
21
+ seen.add(node);
22
+ const text = source.slice(node.startIndex, node.endIndex);
23
+ const firstLine = text.split("\n")[0].trimEnd();
24
+ results.push({
25
+ file: filePath,
26
+ line: node.startPosition.row + 1, // 1-indexed to match JS backend
27
+ col: node.startPosition.column,
28
+ source: firstLine,
29
+ });
30
+ }
31
+ return results;
32
+ }
33
+ export function validateTreeSitterQuery(pattern, language, QueryClass) {
34
+ try {
35
+ assertValidPattern(pattern);
36
+ new QueryClass(language, pattern);
37
+ }
38
+ catch (e) {
39
+ throw new Error(`Invalid tree-sitter query: ${e instanceof Error ? e.message : String(e)}`);
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ export declare const PYTHON_SHORTHANDS: Record<string, string>;
2
+ export declare function expandShorthands(selector: string): string;
@@ -0,0 +1,68 @@
1
+ // Shorthands for common Python AST node types, expressed as tree-sitter
2
+ // S-expression patterns. Each shorthand includes a `@_` capture so that
3
+ // `captures()` returns results without the user needing to add a capture name.
4
+ //
5
+ // For attribute matching or complex patterns, write tree-sitter S-expressions
6
+ // directly (and include at least one capture):
7
+ // (call function: (identifier) @name (#eq? @name "foo"))
8
+ //
9
+ // NOTE: In tree-sitter-python 0.21+, async functions are still
10
+ // `function_definition` (no separate `async_function_definition` type).
11
+ // To match only async functions, use the full query:
12
+ // (function_definition . "async" .) @fn
13
+ export const PYTHON_SHORTHANDS = {
14
+ // Shared semantics with JS backend
15
+ fn: "(function_definition) @_",
16
+ call: "(call) @_",
17
+ class: "(class_definition) @_",
18
+ assign: "(assignment) @_",
19
+ return: "(return_statement) @_",
20
+ await: "(await) @_",
21
+ yield: "(yield) @_",
22
+ import: "(import_statement) @_",
23
+ if: "(if_statement) @_",
24
+ for: "(for_statement) @_",
25
+ while: "(while_statement) @_",
26
+ // Python-specific
27
+ from: "(import_from_statement) @_",
28
+ raise: "(raise_statement) @_",
29
+ with: "(with_statement) @_",
30
+ lambda: "(lambda) @_",
31
+ comp: "(list_comprehension) @_",
32
+ dictcomp: "(dictionary_comprehension) @_",
33
+ setcomp: "(set_comprehension) @_",
34
+ genexp: "(generator_expression) @_",
35
+ decorator: "(decorator) @_",
36
+ assert: "(assert_statement) @_",
37
+ delete: "(delete_statement) @_",
38
+ global: "(global_statement) @_",
39
+ nonlocal: "(nonlocal_statement) @_",
40
+ augassign: "(augmented_assignment) @_",
41
+ decorated: "(decorated_definition) @_",
42
+ };
43
+ export function expandShorthands(selector) {
44
+ // Replace bare shorthand words (not inside quotes and not preceded by @)
45
+ // with their S-expression. The negative lookbehind prevents expanding
46
+ // capture names like @fn into @(function_definition) @_.
47
+ const keys = Object.keys(PYTHON_SHORTHANDS);
48
+ const pattern = new RegExp(`(?<!@)\\b(${keys.join("|")})\\b`, "g");
49
+ const parts = [];
50
+ let i = 0;
51
+ while (i < selector.length) {
52
+ const ch = selector[i];
53
+ if (ch === '"' || ch === "'") {
54
+ let j = i + 1;
55
+ while (j < selector.length && selector[j] !== ch)
56
+ j++;
57
+ parts.push(selector.slice(i, j + 1));
58
+ i = j + 1;
59
+ }
60
+ else {
61
+ const nextQuote = selector.slice(i).search(/['"]/);
62
+ const segment = nextQuote === -1 ? selector.slice(i) : selector.slice(i, i + nextQuote);
63
+ parts.push(segment.replace(pattern, (m) => { var _a; return (_a = PYTHON_SHORTHANDS[m]) !== null && _a !== void 0 ? _a : m; }));
64
+ i = nextQuote === -1 ? selector.length : i + nextQuote;
65
+ }
66
+ }
67
+ return parts.join("");
68
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "ast-search-python",
3
+ "version": "0.1.0",
4
+ "description": "Python language plugin for ast-search",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./build/index.js",
10
+ "types": "./build/index.d.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
16
+ "prepublishOnly": "tsc"
17
+ },
18
+ "author": "shiplet",
19
+ "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/willey-shiplet/ast-search.git",
23
+ "directory": "packages/ast-search-python"
24
+ },
25
+ "files": [
26
+ "build/*.js",
27
+ "build/*.d.ts",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ },
34
+ "dependencies": {
35
+ "tree-sitter": "^0.21.1",
36
+ "tree-sitter-python": "^0.21.0"
37
+ },
38
+ "devDependencies": {
39
+ "@jest/globals": "^29.7.0",
40
+ "@types/jest": "^29.5.12",
41
+ "@types/node": "^20.12.7",
42
+ "ast-search": "workspace:*",
43
+ "jest": "^29.7.0",
44
+ "ts-jest": "^29.1.2",
45
+ "typescript": "^5.4.5"
46
+ },
47
+ "peerDependencies": {
48
+ "ast-search": ">=0.3.0"
49
+ }
50
+ }