@alint-js/languages 0.1.5

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/dist/index.mjs ADDED
@@ -0,0 +1,288 @@
1
+ import { createRequire } from "node:module";
2
+ import { sliceRange, targetIdentity, withLanguage, withStableIdentities } from "@alint-js/core";
3
+ import Parser from "web-tree-sitter";
4
+ import { dirname, join } from "node:path";
5
+ //#region ../plugin/dist/index.mjs
6
+ function definePlugin(plugin) {
7
+ return plugin;
8
+ }
9
+ //#endregion
10
+ //#region src/grammar.ts
11
+ const grammarDir = join(dirname(createRequire(import.meta.url).resolve("tree-sitter-wasms/package.json")), "out");
12
+ /**
13
+ * Which languages this package provides, and the grammar each is parsed with. Adding a language
14
+ * starts here: `LanguageId` is derived from these keys, and `QUERIES` must then cover it too.
15
+ *
16
+ * Ids are spelled as editors spell them, per the convention on `LanguageDefinition.name`.
17
+ * TypeScript and JavaScript are deliberately missing — core's oxc producer already owns those
18
+ * extensions, and `registerLanguage` throws when two plugins claim the same one.
19
+ */
20
+ const GRAMMAR = {
21
+ go: "tree-sitter-go.wasm",
22
+ python: "tree-sitter-python.wasm",
23
+ rust: "tree-sitter-rust.wasm"
24
+ };
25
+ let ready;
26
+ const grammars = /* @__PURE__ */ new Map();
27
+ async function grammarFor(language) {
28
+ ready ??= Parser.init();
29
+ await ready;
30
+ let grammar = grammars.get(language);
31
+ if (grammar === void 0) {
32
+ grammar = await Parser.Language.load(join(grammarDir, GRAMMAR[language]));
33
+ grammars.set(language, grammar);
34
+ }
35
+ return grammar;
36
+ }
37
+ function isLanguageId(value) {
38
+ return Object.hasOwn(GRAMMAR, value);
39
+ }
40
+ const QUERIES = {
41
+ go: `
42
+ (function_declaration) @function
43
+ (method_declaration) @function
44
+ (comment) @comment
45
+ (call_expression function: (identifier) @call)
46
+ (call_expression function: (selector_expression field: (field_identifier) @call))
47
+ (identifier) @identifier
48
+
49
+ (function_declaration name: (identifier) @binder)
50
+ (parameter_declaration name: (identifier) @binder)
51
+ (variadic_parameter_declaration name: (identifier) @binder)
52
+ (short_var_declaration left: (expression_list (identifier) @binder))
53
+ (var_spec name: (identifier) @binder)
54
+ (const_spec name: (identifier) @binder)
55
+ (range_clause left: (expression_list (identifier) @binder))
56
+ `,
57
+ python: `
58
+ (function_definition) @function
59
+ (comment) @comment
60
+ (function_definition body: (block . (expression_statement (string) @comment)))
61
+ (call function: (identifier) @call)
62
+ (call function: (attribute attribute: (identifier) @call))
63
+ (identifier) @identifier
64
+ (attribute attribute: (identifier) @anchor)
65
+ (keyword_argument name: (identifier) @anchor)
66
+
67
+ (function_definition name: (identifier) @binder)
68
+ (parameters (identifier) @binder)
69
+ (default_parameter name: (identifier) @binder)
70
+ (typed_parameter (identifier) @binder)
71
+ (typed_default_parameter name: (identifier) @binder)
72
+ (assignment left: (identifier) @binder)
73
+ (for_statement left: (identifier) @binder)
74
+ (lambda_parameters (identifier) @binder)
75
+ `,
76
+ rust: `
77
+ (function_item) @function
78
+ (line_comment) @comment
79
+ (block_comment) @comment
80
+ (call_expression function: (identifier) @call)
81
+ (call_expression function: (field_expression field: (field_identifier) @call))
82
+ (call_expression function: (scoped_identifier name: (identifier) @call))
83
+ (identifier) @identifier
84
+
85
+ (function_item name: (identifier) @binder)
86
+ (parameter pattern: (identifier) @binder)
87
+ (let_declaration pattern: (identifier) @binder)
88
+ (closure_parameters (identifier) @binder)
89
+ `
90
+ };
91
+ //#endregion
92
+ //#region src/extract.ts
93
+ const queries = /* @__PURE__ */ new Map();
94
+ /**
95
+ * One file target carrying every call site, then one function target per function, each with the
96
+ * `FunctionInfo` a consumer would otherwise need its own parser to work out.
97
+ *
98
+ * The language comes from the file rather than a second argument, because every target carries that
99
+ * same file and the two could then disagree. Pass the file through `withLanguage` first.
100
+ */
101
+ async function extractTargets(file) {
102
+ if (!isLanguageId(file.language)) throw new Error(`Language "${file.language}" is not provided by @alint-js/languages.`);
103
+ const language = file.language;
104
+ const grammar = await grammarFor(language);
105
+ const parser = new Parser();
106
+ parser.setLanguage(grammar);
107
+ const tree = parser.parse(file.text);
108
+ let query = queries.get(language);
109
+ if (query === void 0) {
110
+ query = grammar.query(QUERIES[language]);
111
+ queries.set(language, query);
112
+ }
113
+ const calls = [];
114
+ const comments = [];
115
+ const identifiers = [];
116
+ const anchored = /* @__PURE__ */ new Set();
117
+ const binders = [];
118
+ const functionNodes = [];
119
+ for (const { name, node } of query.captures(tree.rootNode)) switch (name) {
120
+ case "anchor":
121
+ anchored.add(node.startIndex);
122
+ break;
123
+ case "binder":
124
+ binders.push(node);
125
+ break;
126
+ case "call":
127
+ calls.push({
128
+ name: node.text,
129
+ range: rangeOf(node)
130
+ });
131
+ break;
132
+ case "comment":
133
+ comments.push(rangeOf(node));
134
+ break;
135
+ case "function":
136
+ functionNodes.push(node);
137
+ break;
138
+ case "identifier":
139
+ identifiers.push(rangeOf(node));
140
+ break;
141
+ }
142
+ const renameable = identifiers.filter((range) => !anchored.has(range.start));
143
+ const commentStarts = new Set(comments.map((range) => range.start));
144
+ const functionTargets = functionNodes.map((node) => functionTarget(node, file, language, comments, commentStarts, renameable, binders));
145
+ return [fileTarget(file, calls), ...withStableIdentities(functionTargets)];
146
+ }
147
+ /**
148
+ * The body's statements, comments left out.
149
+ *
150
+ * All three grammars make a comment a named child of the block it sits in, so comments have to be
151
+ * filtered out here. Leave them in and adding a comment to a one-line function makes it look two
152
+ * statements long.
153
+ */
154
+ function bodyStatements(node, commentStarts) {
155
+ const body = node.childForFieldName("body");
156
+ if (body === null) return [];
157
+ const statements = [];
158
+ for (let index = 0; index < body.namedChildCount; index += 1) {
159
+ const child = body.namedChild(index);
160
+ if (child !== null && !commentStarts.has(child.startIndex)) statements.push(child);
161
+ }
162
+ return statements;
163
+ }
164
+ function fileTarget(file, calls) {
165
+ return {
166
+ file,
167
+ identity: "file",
168
+ kind: "file",
169
+ language: file.language,
170
+ metadata: { calls },
171
+ origin: { physicalPath: file.path },
172
+ text: file.text
173
+ };
174
+ }
175
+ function functionTarget(node, file, language, comments, commentStarts, identifiers, binders) {
176
+ const range = rangeOf(node);
177
+ const source = sliceRange(file, range);
178
+ const nameNode = node.childForFieldName("name");
179
+ const name = nameNode?.text;
180
+ const statements = bodyStatements(node, commentStarts);
181
+ const exported = isExported(node, language, name ?? "");
182
+ const info = {
183
+ bodyIsSingleExpression: statements.length === 1 && !holdsBlock(statements[0]),
184
+ bodyStatements: statements.length,
185
+ commentRanges: rangesInside(comments, range),
186
+ declaredNames: [.../* @__PURE__ */ new Set([...binders.filter((binder) => binder.startIndex >= range.start && binder.endIndex <= range.end).map((binder) => binder.text), ...name === void 0 ? [] : [name]])],
187
+ exported,
188
+ identifierRanges: withOwnName(rangesInside(identifiers, range), nameNode, range)
189
+ };
190
+ return {
191
+ file,
192
+ identity: targetIdentity("function", name, range),
193
+ kind: "function",
194
+ language: file.language,
195
+ loc: source.loc,
196
+ metadata: {
197
+ exported,
198
+ function: info
199
+ },
200
+ name,
201
+ origin: {
202
+ physicalPath: file.path,
203
+ range
204
+ },
205
+ range,
206
+ text: source.text
207
+ };
208
+ }
209
+ /** Whether anything under this node is a block, which is how all three grammars write a branch or a loop. */
210
+ function holdsBlock(node) {
211
+ const pending = [...node.namedChildren];
212
+ while (pending.length > 0) {
213
+ const next = pending.pop();
214
+ if (next === void 0) continue;
215
+ if (next.type === "block") return true;
216
+ pending.push(...next.namedChildren);
217
+ }
218
+ return false;
219
+ }
220
+ /** Reachable from outside its file. Go says it with a capital, Python with the absence of a leading underscore, Rust with `pub`. */
221
+ function isExported(node, language, name) {
222
+ switch (language) {
223
+ case "go": return /^[A-Z]/.test(name);
224
+ case "python": return !name.startsWith("_");
225
+ case "rust": return node.children.some((child) => child.type === "visibility_modifier");
226
+ }
227
+ }
228
+ /** `web-tree-sitter` counts in JavaScript string indexes, not UTF-8 bytes, so `text.slice(start, end)` is correct. */
229
+ function rangeOf(node) {
230
+ return {
231
+ end: node.endIndex,
232
+ start: node.startIndex
233
+ };
234
+ }
235
+ /** Rebases the ranges inside `outer` onto the function's own `text`, which is where `FunctionInfo` says they point. */
236
+ function rangesInside(ranges, outer) {
237
+ return ranges.filter((range) => range.start >= outer.start && range.end <= outer.end).map((range) => ({
238
+ end: range.end - outer.start,
239
+ start: range.start - outer.start
240
+ }));
241
+ }
242
+ /**
243
+ * Adds the function's own name to its renameable identifiers.
244
+ *
245
+ * This only has anything to add for a Go method, whose name is a `field_identifier` rather than an
246
+ * `identifier`. The Go query leaves `field_identifier` out on purpose, because reading a struct
247
+ * field is not renameable, but a method naming itself is. Every other name the queries capture is
248
+ * already an `@identifier`, so the dedupe by start offset drops the second copy.
249
+ */
250
+ function withOwnName(ranges, nameNode, outer) {
251
+ if (nameNode === null) return ranges;
252
+ const name = {
253
+ end: nameNode.endIndex - outer.start,
254
+ start: nameNode.startIndex - outer.start
255
+ };
256
+ if (ranges.some((range) => range.start === name.start)) return ranges;
257
+ return [...ranges, name].sort((left, right) => left.start - right.start);
258
+ }
259
+ //#endregion
260
+ //#region src/index.ts
261
+ const goLanguage = {
262
+ extensions: [".go"],
263
+ extract: (file) => extractTargets(withLanguage(file, "go")),
264
+ name: "go"
265
+ };
266
+ const pythonLanguage = {
267
+ extensions: [".py"],
268
+ extract: (file) => extractTargets(withLanguage(file, "python")),
269
+ name: "python"
270
+ };
271
+ const rustLanguage = {
272
+ extensions: [".rs"],
273
+ extract: (file) => extractTargets(withLanguage(file, "rust")),
274
+ name: "rust"
275
+ };
276
+ /**
277
+ * A plugin that declares languages and nothing else: no rules, no processors.
278
+ *
279
+ * The alias a config registers it under does not matter, because languages register under their own
280
+ * names. Once registered, they serve every plugin's rules, not just this one's.
281
+ */
282
+ const languagesPlugin = definePlugin({ languages: {
283
+ go: goLanguage,
284
+ python: pythonLanguage,
285
+ rust: rustLanguage
286
+ } });
287
+ //#endregion
288
+ export { languagesPlugin as default, languagesPlugin, extractTargets, goLanguage, pythonLanguage, rustLanguage };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@alint-js/languages",
3
+ "type": "module",
4
+ "version": "0.1.5",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.mts",
8
+ "default": "./dist/index.mjs"
9
+ },
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "peerDependencies": {
16
+ "@alint-js/core": "0.1.5"
17
+ },
18
+ "dependencies": {
19
+ "tree-sitter-wasms": "^0.1.13",
20
+ "web-tree-sitter": "^0.24.7"
21
+ },
22
+ "devDependencies": {
23
+ "@alint-js/core": "0.1.5",
24
+ "@alint-js/plugin": "0.1.5"
25
+ },
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "test": "vitest run --config vitest.config.ts"
30
+ }
31
+ }