@goodbones/ast-grep 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Data Quail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export { astGrepMatcher, type AstGrepOptions, TYPESCRIPT_LANGUAGES } from "./matcher.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Lang } from "@ast-grep/napi";
2
+ import type { SyntaxMatcher } from "@goodbones/core";
3
+ export type AstGrepOptions = {
4
+ readonly languages: Readonly<Record<string, Lang>>;
5
+ };
6
+ export declare const TYPESCRIPT_LANGUAGES: Readonly<Record<string, Lang>>;
7
+ export declare const astGrepMatcher: (options?: AstGrepOptions) => SyntaxMatcher;
8
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAsB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,KAAK,EAAyB,aAAa,EAAc,MAAM,iBAAiB,CAAC;AAcxF,MAAM,MAAM,cAAc,GAAG;IAE3B,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;CACpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAS/D,CAAC;AAkJF,eAAO,MAAM,cAAc,aAChB,cAAc,KACtB,aAuBD,CAAC"}
@@ -0,0 +1,7 @@
1
+ // The ast-grep syntax matcher, as one package: a campaign's `syntax` term
2
+ // evaluated through `@ast-grep/napi`, behind the core's `SyntaxMatcher` port.
3
+ // A host composes it into a language pack (`typescriptLanguage({ syntax:
4
+ // astGrepMatcher() })`); the pack never names it, and the core never imports
5
+ // it. A second engine over another tree is a second package shaped like this.
6
+ export { astGrepMatcher, TYPESCRIPT_LANGUAGES } from "./matcher.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8EAA8E;AAC9E,yEAAyE;AACzE,6EAA6E;AAC7E,8EAA8E;AAC9E,OAAO,EAAE,cAAc,EAAuB,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,165 @@
1
+ import { Lang, parse } from "@ast-grep/napi";
2
+ export const TYPESCRIPT_LANGUAGES = {
3
+ ".ts": Lang.TypeScript,
4
+ ".mts": Lang.TypeScript,
5
+ ".cts": Lang.TypeScript,
6
+ ".tsx": Lang.Tsx,
7
+ ".js": Lang.JavaScript,
8
+ ".mjs": Lang.JavaScript,
9
+ ".cjs": Lang.JavaScript,
10
+ ".jsx": Lang.JavaScript,
11
+ };
12
+ // The tree-sitter kinds that declare a name, and where the name sits. A
13
+ // class, function, method, variable declarator, type alias, interface, enum
14
+ // or module carries it in its `name` field.
15
+ const DECLARATION_KINDS = new Set([
16
+ "class_declaration",
17
+ "abstract_class_declaration",
18
+ "function_declaration",
19
+ "generator_function_declaration",
20
+ "method_definition",
21
+ "method_signature",
22
+ "variable_declarator",
23
+ "type_alias_declaration",
24
+ "interface_declaration",
25
+ "enum_declaration",
26
+ "internal_module",
27
+ "module",
28
+ ]);
29
+ // `kind()` is typed as a union over the grammar's static map; what it holds
30
+ // at runtime is the kind's name.
31
+ const kindOf = (node) => String(node.kind());
32
+ const nameOf = (node) => {
33
+ try {
34
+ const name = node.field("name");
35
+ return name === null ? null : name.text();
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ };
41
+ // The match itself when it is a named declaration, else the nearest ancestor
42
+ // that is one; `null` at the top level of the file.
43
+ const anchorOf = (node) => {
44
+ if (DECLARATION_KINDS.has(kindOf(node))) {
45
+ const own = nameOf(node);
46
+ if (own !== null)
47
+ return own;
48
+ }
49
+ for (const ancestor of node.ancestors()) {
50
+ if (!DECLARATION_KINDS.has(kindOf(ancestor)))
51
+ continue;
52
+ const name = nameOf(ancestor);
53
+ if (name !== null)
54
+ return name;
55
+ }
56
+ return null;
57
+ };
58
+ // A metavariable of the rule: `$NAME` in a pattern, or a key of `constraints`.
59
+ // ast-grep reports captures by name on request rather than as a map, so the
60
+ // names are read off the rule's text and asked for one by one.
61
+ const METAVARIABLE = /\$([A-Z_][A-Z0-9_]*)/g;
62
+ const metavariablesOf = (rule) => {
63
+ const found = new Set();
64
+ for (const match of JSON.stringify(rule).matchAll(METAVARIABLE)) {
65
+ if (match[1] !== undefined)
66
+ found.add(match[1]);
67
+ }
68
+ return [...found];
69
+ };
70
+ const capturesOf = (node, names) => {
71
+ const captures = new Map();
72
+ for (const name of names) {
73
+ const single = node.getMatch(name);
74
+ if (single !== null) {
75
+ captures.set(name, single.text());
76
+ continue;
77
+ }
78
+ const several = node.getMultipleMatches(name);
79
+ if (several.length > 0)
80
+ captures.set(name, several.map((one) => one.text()).join(""));
81
+ }
82
+ return captures;
83
+ };
84
+ const matchOf = (node, names) => {
85
+ const range = node.range();
86
+ return {
87
+ text: node.text(),
88
+ captures: capturesOf(node, names),
89
+ range: {
90
+ start: { line: range.start.line, column: range.start.column },
91
+ end: { line: range.end.line, column: range.end.column },
92
+ },
93
+ anchor: anchorOf(node),
94
+ };
95
+ };
96
+ const declarationsOf = (root) => {
97
+ const found = [];
98
+ for (const kind of DECLARATION_KINDS) {
99
+ let nodes;
100
+ try {
101
+ nodes = root.findAll({ rule: { kind } });
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ for (const node of nodes) {
107
+ const name = nameOf(node);
108
+ if (name === null)
109
+ continue;
110
+ const range = node.range();
111
+ found.push({
112
+ name,
113
+ start: { line: range.start.line, column: range.start.column },
114
+ end: { line: range.end.line, column: range.end.column },
115
+ });
116
+ }
117
+ }
118
+ return found;
119
+ };
120
+ const before = (left, right) => left.line < right.line || (left.line === right.line && left.column <= right.column);
121
+ const contains = (one, at) => before(one.start, at) && before(at, one.end);
122
+ // The innermost declaration containing the position: of those that do, the
123
+ // one that starts last.
124
+ const anchorAtOf = (declared, at) => {
125
+ let innermost = null;
126
+ for (const one of declared) {
127
+ if (!contains(one, at))
128
+ continue;
129
+ if (innermost === null || before(innermost.start, one.start))
130
+ innermost = one;
131
+ }
132
+ return innermost === null ? null : innermost.name;
133
+ };
134
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
135
+ const extensionOf = (file) => {
136
+ const base = file.slice(file.lastIndexOf("/") + 1);
137
+ const dot = base.lastIndexOf(".");
138
+ return dot === -1 ? "" : base.slice(dot);
139
+ };
140
+ export const astGrepMatcher = (options = { languages: TYPESCRIPT_LANGUAGES }) => ({
141
+ parse: (file, text) => {
142
+ const language = options.languages[extensionOf(file)];
143
+ if (language === undefined)
144
+ return null;
145
+ const root = parse(language, text).root();
146
+ let declared = null;
147
+ return {
148
+ anchorAt: (position) => {
149
+ declared ??= declarationsOf(root);
150
+ return anchorAtOf(declared, position);
151
+ },
152
+ findAll: (rule) => {
153
+ if (!isRecord(rule)) {
154
+ throw new Error(`a syntax rule is an object of ast-grep rule keys, not ${typeof rule}`);
155
+ }
156
+ // The engine refuses a rule it cannot read with its own sentence; it
157
+ // is thrown as-is, since the probe check is what turns it into a
158
+ // load failure that names the campaign.
159
+ const names = metavariablesOf(rule);
160
+ return root.findAll({ rule }).map((node) => matchOf(node, names));
161
+ },
162
+ };
163
+ },
164
+ });
165
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/matcher.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@goodbones/ast-grep",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "The ast-grep syntax matcher for @goodbones/core: a campaign's `syntax` term, evaluated through @ast-grep/napi behind the core's SyntaxMatcher port.",
7
+ "author": "zacharyweidenbach",
8
+ "keywords": [
9
+ "architecture",
10
+ "lint",
11
+ "ast-grep",
12
+ "campaigns",
13
+ "monorepo"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/dataquail/goodbones.git",
21
+ "directory": "packages/ast-grep"
22
+ },
23
+ "homepage": "https://dataquail.github.io/goodbones",
24
+ "bugs": {
25
+ "url": "https://github.com/dataquail/goodbones/issues"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./build/dts/index.d.ts",
30
+ "default": "./build/esm/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "types": "./build/dts/index.d.ts",
35
+ "files": [
36
+ "build/dts",
37
+ "build/esm",
38
+ "src",
39
+ "!src/**/*.test.ts"
40
+ ],
41
+ "dependencies": {
42
+ "@ast-grep/napi": "^0.45.3",
43
+ "@goodbones/core": "0.1.0-beta.8"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.9.4"
47
+ },
48
+ "nx": {
49
+ "includedScripts": []
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -b tsconfig.build.json",
53
+ "check": "tsc -b tsconfig.json",
54
+ "clean": "rm -rf build .tsbuildinfo",
55
+ "test": "vitest run",
56
+ "coverage": "vitest run --coverage"
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // The ast-grep syntax matcher, as one package: a campaign's `syntax` term
2
+ // evaluated through `@ast-grep/napi`, behind the core's `SyntaxMatcher` port.
3
+ // A host composes it into a language pack (`typescriptLanguage({ syntax:
4
+ // astGrepMatcher() })`); the pack never names it, and the core never imports
5
+ // it. A second engine over another tree is a second package shaped like this.
6
+ export { astGrepMatcher, type AstGrepOptions, TYPESCRIPT_LANGUAGES } from "./matcher.js";
package/src/matcher.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { Lang, parse, type SgNode } from "@ast-grep/napi";
2
+ import type { Position, SyntaxMatch, SyntaxMatcher, SyntaxTree } from "@goodbones/core";
3
+
4
+ // The core's `SyntaxMatcher` port over ast-grep. A campaign's `syntax` term is
5
+ // an ast-grep rule object, passed through as the `rule` of a `NapiConfig`;
6
+ // each node it matches becomes a `SyntaxMatch` with its text, what every
7
+ // metavariable captured, its range, and the nearest enclosing named
8
+ // declaration as its anchor. Nothing here knows about imports: narrowing a
9
+ // capture by what it is bound to is the core's, against the file's facts.
10
+ //
11
+ // The `kind` names a rule may use are tree-sitter's for the grammar ast-grep
12
+ // parses the file with (`class_declaration`, `call_expression`), and so is
13
+ // the anchor's notion of a declaration below. A matcher over another tree
14
+ // would ship its own mapping.
15
+
16
+ export type AstGrepOptions = {
17
+ // File extension (with the dot) to ast-grep language id.
18
+ readonly languages: Readonly<Record<string, Lang>>;
19
+ };
20
+
21
+ export const TYPESCRIPT_LANGUAGES: Readonly<Record<string, Lang>> = {
22
+ ".ts": Lang.TypeScript,
23
+ ".mts": Lang.TypeScript,
24
+ ".cts": Lang.TypeScript,
25
+ ".tsx": Lang.Tsx,
26
+ ".js": Lang.JavaScript,
27
+ ".mjs": Lang.JavaScript,
28
+ ".cjs": Lang.JavaScript,
29
+ ".jsx": Lang.JavaScript,
30
+ };
31
+
32
+ // The tree-sitter kinds that declare a name, and where the name sits. A
33
+ // class, function, method, variable declarator, type alias, interface, enum
34
+ // or module carries it in its `name` field.
35
+ const DECLARATION_KINDS: ReadonlySet<string> = new Set([
36
+ "class_declaration",
37
+ "abstract_class_declaration",
38
+ "function_declaration",
39
+ "generator_function_declaration",
40
+ "method_definition",
41
+ "method_signature",
42
+ "variable_declarator",
43
+ "type_alias_declaration",
44
+ "interface_declaration",
45
+ "enum_declaration",
46
+ "internal_module",
47
+ "module",
48
+ ]);
49
+
50
+ // `kind()` is typed as a union over the grammar's static map; what it holds
51
+ // at runtime is the kind's name.
52
+ const kindOf = (node: SgNode): string => String(node.kind());
53
+
54
+ const nameOf = (node: SgNode): string | null => {
55
+ try {
56
+ const name = (node as SgNode & { field: (name: string) => SgNode | null }).field("name");
57
+ return name === null ? null : name.text();
58
+ } catch {
59
+ return null;
60
+ }
61
+ };
62
+
63
+ // The match itself when it is a named declaration, else the nearest ancestor
64
+ // that is one; `null` at the top level of the file.
65
+ const anchorOf = (node: SgNode): string | null => {
66
+ if (DECLARATION_KINDS.has(kindOf(node))) {
67
+ const own = nameOf(node);
68
+ if (own !== null) return own;
69
+ }
70
+ for (const ancestor of node.ancestors()) {
71
+ if (!DECLARATION_KINDS.has(kindOf(ancestor))) continue;
72
+ const name = nameOf(ancestor);
73
+ if (name !== null) return name;
74
+ }
75
+ return null;
76
+ };
77
+
78
+ // A metavariable of the rule: `$NAME` in a pattern, or a key of `constraints`.
79
+ // ast-grep reports captures by name on request rather than as a map, so the
80
+ // names are read off the rule's text and asked for one by one.
81
+ const METAVARIABLE = /\$([A-Z_][A-Z0-9_]*)/g;
82
+
83
+ const metavariablesOf = (rule: unknown): ReadonlyArray<string> => {
84
+ const found = new Set<string>();
85
+ for (const match of JSON.stringify(rule).matchAll(METAVARIABLE)) {
86
+ if (match[1] !== undefined) found.add(match[1]);
87
+ }
88
+ return [...found];
89
+ };
90
+
91
+ const capturesOf = (node: SgNode, names: ReadonlyArray<string>): ReadonlyMap<string, string> => {
92
+ const captures = new Map<string, string>();
93
+ for (const name of names) {
94
+ const single = node.getMatch(name);
95
+ if (single !== null) {
96
+ captures.set(name, single.text());
97
+ continue;
98
+ }
99
+ const several = node.getMultipleMatches(name);
100
+ if (several.length > 0) captures.set(name, several.map((one) => one.text()).join(""));
101
+ }
102
+ return captures;
103
+ };
104
+
105
+ const matchOf = (node: SgNode, names: ReadonlyArray<string>): SyntaxMatch => {
106
+ const range = node.range();
107
+ return {
108
+ text: node.text(),
109
+ captures: capturesOf(node, names),
110
+ range: {
111
+ start: { line: range.start.line, column: range.start.column },
112
+ end: { line: range.end.line, column: range.end.column },
113
+ },
114
+ anchor: anchorOf(node),
115
+ };
116
+ };
117
+
118
+ // Every named declaration in the tree, innermost last, for anchoring a
119
+ // position. Found once per tree; a kind the grammar does not have (a type
120
+ // alias in JavaScript) is skipped.
121
+ type Declared = {
122
+ readonly name: string;
123
+ readonly start: Position;
124
+ readonly end: Position;
125
+ };
126
+
127
+ const declarationsOf = (root: SgNode): ReadonlyArray<Declared> => {
128
+ const found: Array<Declared> = [];
129
+ for (const kind of DECLARATION_KINDS) {
130
+ let nodes: ReadonlyArray<SgNode>;
131
+ try {
132
+ nodes = root.findAll({ rule: { kind } });
133
+ } catch {
134
+ continue;
135
+ }
136
+ for (const node of nodes) {
137
+ const name = nameOf(node);
138
+ if (name === null) continue;
139
+ const range = node.range();
140
+ found.push({
141
+ name,
142
+ start: { line: range.start.line, column: range.start.column },
143
+ end: { line: range.end.line, column: range.end.column },
144
+ });
145
+ }
146
+ }
147
+ return found;
148
+ };
149
+
150
+ const before = (left: Position, right: Position): boolean =>
151
+ left.line < right.line || (left.line === right.line && left.column <= right.column);
152
+
153
+ const contains = (one: Declared, at: Position): boolean =>
154
+ before(one.start, at) && before(at, one.end);
155
+
156
+ // The innermost declaration containing the position: of those that do, the
157
+ // one that starts last.
158
+ const anchorAtOf = (declared: ReadonlyArray<Declared>, at: Position): string | null => {
159
+ let innermost: Declared | null = null;
160
+ for (const one of declared) {
161
+ if (!contains(one, at)) continue;
162
+ if (innermost === null || before(innermost.start, one.start)) innermost = one;
163
+ }
164
+ return innermost === null ? null : innermost.name;
165
+ };
166
+
167
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
168
+ typeof value === "object" && value !== null && !Array.isArray(value);
169
+
170
+ const extensionOf = (file: string): string => {
171
+ const base = file.slice(file.lastIndexOf("/") + 1);
172
+ const dot = base.lastIndexOf(".");
173
+ return dot === -1 ? "" : base.slice(dot);
174
+ };
175
+
176
+ export const astGrepMatcher = (
177
+ options: AstGrepOptions = { languages: TYPESCRIPT_LANGUAGES },
178
+ ): SyntaxMatcher => ({
179
+ parse: (file, text): SyntaxTree | null => {
180
+ const language = options.languages[extensionOf(file)];
181
+ if (language === undefined) return null;
182
+ const root = parse(language, text).root();
183
+ let declared: ReadonlyArray<Declared> | null = null;
184
+ return {
185
+ anchorAt: (position) => {
186
+ declared ??= declarationsOf(root);
187
+ return anchorAtOf(declared, position);
188
+ },
189
+ findAll: (rule) => {
190
+ if (!isRecord(rule)) {
191
+ throw new Error(`a syntax rule is an object of ast-grep rule keys, not ${typeof rule}`);
192
+ }
193
+ // The engine refuses a rule it cannot read with its own sentence; it
194
+ // is thrown as-is, since the probe check is what turns it into a
195
+ // load failure that names the campaign.
196
+ const names = metavariablesOf(rule);
197
+ return root.findAll({ rule }).map((node) => matchOf(node, names));
198
+ },
199
+ };
200
+ },
201
+ });