dsh-plugin-codegraph-tree-sitter 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 +21 -0
- package/README.md +17 -0
- package/lib/extract.d.ts +70 -0
- package/lib/extract.js +296 -0
- package/lib/git-hooks.d.ts +52 -0
- package/lib/git-hooks.js +75 -0
- package/lib/gitignore.d.ts +58 -0
- package/lib/gitignore.js +107 -0
- package/lib/grammar.d.ts +27 -0
- package/lib/grammar.js +51 -0
- package/lib/index.d.ts +112 -0
- package/lib/index.js +203 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +25 -0
- package/lib/languages.d.ts +62 -0
- package/lib/languages.js +107 -0
- package/lib/resolve.d.ts +89 -0
- package/lib/resolve.js +218 -0
- package/lib/schema.d.ts +35 -0
- package/lib/schema.js +126 -0
- package/lib/walk.d.ts +57 -0
- package/lib/walk.js +146 -0
- package/lib/watch-policy.d.ts +49 -0
- package/lib/watch-policy.js +64 -0
- package/lib/watcher.d.ts +95 -0
- package/lib/watcher.js +299 -0
- package/lib/worktree.d.ts +41 -0
- package/lib/worktree.js +63 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CC ZHAO
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dsh-plugin-codegraph-tree-sitter
|
|
2
|
+
|
|
3
|
+
Self-built code-graph indexer. Parses TypeScript, TSX, JavaScript, JSX, Python, and Go with `web-tree-sitter` and writes schema version 4 to `.codegraph/codegraph.db`, so a workspace needs no external indexer. Registers only an indexer, never a store.
|
|
4
|
+
|
|
5
|
+
Part of **[dsh-plugin-codegraph](https://github.com/CC19990113/dsh-plugin-codegraph)** — structural code intelligence for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
|
|
6
|
+
|
|
7
|
+
Most users should install the bundle instead, which mounts this package and its siblings in one layer:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
dsh plugin --profile <name> add dsh-plugin-codegraph
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See the [project README](https://github.com/CC19990113/dsh-plugin-codegraph#readme) for setup, configuration, and the full tool reference.
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
[MIT](LICENSE)
|
package/lib/extract.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-file extraction: walk a parsed tree exactly once, collecting the definitions, call sites, and
|
|
3
|
+
* import bindings the two-pass resolver in `resolve.ts` needs.
|
|
4
|
+
*
|
|
5
|
+
* The caller of a call node is the nearest enclosing definition, or the file itself when the call sits
|
|
6
|
+
* at module top level — the on-disk format records it that way, and a third of the call edges in a
|
|
7
|
+
* real workspace are of exactly this shape.
|
|
8
|
+
* @module dsh-plugin-codegraph-tree-sitter/extract
|
|
9
|
+
*/
|
|
10
|
+
import type { Tree } from 'web-tree-sitter';
|
|
11
|
+
import type { LanguageSpec } from './languages.ts';
|
|
12
|
+
/** One declaration this file introduces, before cross-file resolution. */
|
|
13
|
+
export interface RawDefinition {
|
|
14
|
+
/** Stable position-derived key, unique within the file. */
|
|
15
|
+
readonly key: string;
|
|
16
|
+
/** The immediately enclosing definition's {@link key}, or `null` for a module-top-level definition. */
|
|
17
|
+
readonly parentKey: string | null;
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/** Enclosing definition names, outermost first; used to build `qualifiedName`. */
|
|
21
|
+
readonly container: readonly string[];
|
|
22
|
+
readonly startLine: number;
|
|
23
|
+
readonly endLine: number;
|
|
24
|
+
readonly startColumn: number;
|
|
25
|
+
readonly endColumn: number;
|
|
26
|
+
readonly isExported: boolean;
|
|
27
|
+
readonly isAsync: boolean;
|
|
28
|
+
readonly isStatic: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** One call site this file contains, before callee resolution. */
|
|
31
|
+
export interface RawCall {
|
|
32
|
+
/** The calling definition's {@link RawDefinition.key}, or `null` for a module-top-level call. */
|
|
33
|
+
readonly callerKey: string | null;
|
|
34
|
+
/** The callee's simple name, e.g. `parse` from `parse(x)` or `obj.parse(x)`. */
|
|
35
|
+
readonly calleeName: string;
|
|
36
|
+
readonly line: number;
|
|
37
|
+
readonly column: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the callee expression is a member access (`obj.parse()`) rather than a bare identifier
|
|
40
|
+
* (`parse()`). With no type information, a member call's receiver could be anything, so a name
|
|
41
|
+
* match against it is far less trustworthy than a bare identifier's — `resolve.ts` uses this to
|
|
42
|
+
* separate that noise from a genuine gap in the graph when a call goes unresolved.
|
|
43
|
+
*/
|
|
44
|
+
readonly isMemberCall: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** One import binding this file introduces. */
|
|
47
|
+
export interface RawImport {
|
|
48
|
+
/** The name this file's scope binds, after any `as` rename. */
|
|
49
|
+
readonly localName: string;
|
|
50
|
+
/** The name as declared in the source module, `'default'` for a default import, or `'*'` for a
|
|
51
|
+
* namespace or whole-package import — the latter two never drive resolution but are recorded for
|
|
52
|
+
* completeness. */
|
|
53
|
+
readonly importedName: string;
|
|
54
|
+
/** The raw module specifier text, e.g. `./bar` or `fmt`. */
|
|
55
|
+
readonly specifier: string;
|
|
56
|
+
}
|
|
57
|
+
/** Everything one file's walk produced. */
|
|
58
|
+
export interface FileExtraction {
|
|
59
|
+
readonly definitions: RawDefinition[];
|
|
60
|
+
readonly calls: RawCall[];
|
|
61
|
+
readonly imports: RawImport[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract every definition, call, and import from one parsed file.
|
|
65
|
+
* @param tree - the file's parsed syntax tree.
|
|
66
|
+
* @param spec - the language's extraction table entry.
|
|
67
|
+
* @returns the raw, not-yet-resolved extraction.
|
|
68
|
+
*/
|
|
69
|
+
export declare function extractFile(tree: Tree, spec: LanguageSpec): FileExtraction;
|
|
70
|
+
//# sourceMappingURL=extract.d.ts.map
|
package/lib/extract.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-file extraction: walk a parsed tree exactly once, collecting the definitions, call sites, and
|
|
3
|
+
* import bindings the two-pass resolver in `resolve.ts` needs.
|
|
4
|
+
*
|
|
5
|
+
* The caller of a call node is the nearest enclosing definition, or the file itself when the call sits
|
|
6
|
+
* at module top level — the on-disk format records it that way, and a third of the call edges in a
|
|
7
|
+
* real workspace are of exactly this shape.
|
|
8
|
+
* @module dsh-plugin-codegraph-tree-sitter/extract
|
|
9
|
+
*/
|
|
10
|
+
/** A node's named children, with the `null` slots `web-tree-sitter` reserves for missing nodes dropped. */
|
|
11
|
+
function namedChildren(node) {
|
|
12
|
+
return node.namedChildren.filter(child => child !== null);
|
|
13
|
+
}
|
|
14
|
+
/** Every descendant of `node` matching `types`, with `null` slots dropped. */
|
|
15
|
+
function descendantsOfType(node, types) {
|
|
16
|
+
return node.descendantsOfType([...types]).filter(child => child !== null);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A field's text, or `fallback` when the field is absent. `childForFieldName` is typed to return
|
|
20
|
+
* `Node | null` for any field name on any node, but a specific field is absent only when the caller
|
|
21
|
+
* asks for one the matched node type does not carry.
|
|
22
|
+
* @param node - the node to read a field from.
|
|
23
|
+
* @param field - the field name.
|
|
24
|
+
* @param fallback - the text to use when the field is absent.
|
|
25
|
+
* @returns the field's text, or `fallback`.
|
|
26
|
+
*/
|
|
27
|
+
function fieldText(node, field, fallback) {
|
|
28
|
+
return node.childForFieldName(field)?.text ?? fallback;
|
|
29
|
+
}
|
|
30
|
+
/** Whether a node carries a modifier keyword as one of its own (non-named) children. */
|
|
31
|
+
function hasKeywordChild(node, keyword) {
|
|
32
|
+
for (let index = 0; index < node.childCount; index++) {
|
|
33
|
+
const child = node.child(index);
|
|
34
|
+
if (child !== null && !child.isNamed && child.text === keyword)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/** The definition rule matching `node`, or `undefined` when it introduces no declaration. */
|
|
40
|
+
function matchDefinition(node, definitions) {
|
|
41
|
+
for (const rule of definitions) {
|
|
42
|
+
if (node.type !== rule.nodeType)
|
|
43
|
+
continue;
|
|
44
|
+
if (rule.value !== undefined) {
|
|
45
|
+
const value = node.childForFieldName(rule.value.field);
|
|
46
|
+
if (value === null || !rule.value.types.includes(value.type))
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
return rule;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The callee's simple name from a call node's function-field expression: the identifier itself, or
|
|
55
|
+
* the rightmost property/field/selector name for a member access — `obj.parse()` resolves against
|
|
56
|
+
* `parse`, the same ambiguity a workspace-wide name search already accepts and reports through
|
|
57
|
+
* `unresolvedCount` when it cannot be settled.
|
|
58
|
+
* @param callee - the call node's function-field expression.
|
|
59
|
+
* @returns the simple name, or `undefined` when the expression names nothing identifier-shaped (a
|
|
60
|
+
* computed or parenthesized callee, for instance).
|
|
61
|
+
*/
|
|
62
|
+
function calleeName(callee) {
|
|
63
|
+
if (callee.type === 'identifier')
|
|
64
|
+
return callee.text;
|
|
65
|
+
const property = callee.childForFieldName('property')
|
|
66
|
+
?? callee.childForFieldName('field')
|
|
67
|
+
?? callee.childForFieldName('attribute');
|
|
68
|
+
if (property !== null && property.type !== 'computed_property_name')
|
|
69
|
+
return property.text;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/** ECMAScript-family import extraction: `import_statement` with a `import_clause` and a `source`. */
|
|
73
|
+
function ecmascriptImports(node) {
|
|
74
|
+
// `source` is required by the grammar; the null case only satisfies `childForFieldName`'s general
|
|
75
|
+
// `Node | null` return type.
|
|
76
|
+
const source = node.childForFieldName('source');
|
|
77
|
+
/* v8 ignore next */
|
|
78
|
+
if (source === null)
|
|
79
|
+
return [];
|
|
80
|
+
// An empty string literal (`import ''`) parses with no `string_fragment` child.
|
|
81
|
+
const specifier = namedChildren(source)[0]?.text ?? source.text.slice(1, -1);
|
|
82
|
+
// A side-effect-only import (`import './side'`) carries no `import_clause`.
|
|
83
|
+
const clause = namedChildren(node).find(child => child.type === 'import_clause');
|
|
84
|
+
if (clause === undefined)
|
|
85
|
+
return [];
|
|
86
|
+
const imports = [];
|
|
87
|
+
for (const child of namedChildren(clause)) {
|
|
88
|
+
if (child.type === 'identifier') {
|
|
89
|
+
imports.push({ localName: child.text, importedName: 'default', specifier });
|
|
90
|
+
}
|
|
91
|
+
if (child.type === 'namespace_import') {
|
|
92
|
+
// The grammar requires `* as <identifier>` together; a `namespace_import` node is never
|
|
93
|
+
// produced with no named child.
|
|
94
|
+
const local = namedChildren(child)[0];
|
|
95
|
+
/* v8 ignore next */
|
|
96
|
+
if (local === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
imports.push({ localName: local.text, importedName: '*', specifier });
|
|
99
|
+
}
|
|
100
|
+
if (child.type !== 'named_imports')
|
|
101
|
+
continue;
|
|
102
|
+
// The grammar allows only `import_specifier` as a `named_imports` child; `{}` produces zero
|
|
103
|
+
// named children, never one of a different type.
|
|
104
|
+
for (const specifierNode of namedChildren(child)) {
|
|
105
|
+
/* v8 ignore next */
|
|
106
|
+
if (specifierNode.type !== 'import_specifier')
|
|
107
|
+
continue;
|
|
108
|
+
// `name` is required by the grammar; only `alias` is conditional on an `as` clause.
|
|
109
|
+
const name = specifierNode.childForFieldName('name');
|
|
110
|
+
const alias = specifierNode.childForFieldName('alias');
|
|
111
|
+
/* v8 ignore next */
|
|
112
|
+
if (name === null)
|
|
113
|
+
continue;
|
|
114
|
+
imports.push({ localName: alias?.text ?? name.text, importedName: name.text, specifier });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return imports;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* One `dotted_name` or `aliased_import` child of a Python import statement, resolved to a binding.
|
|
121
|
+
* @param child - the `dotted_name` or `aliased_import` node.
|
|
122
|
+
* @param bindsSymbol - whether this statement binds one module-level name (`from x import y`) rather
|
|
123
|
+
* than the whole module (`import x`), which decides whether the binding's `importedName` is the
|
|
124
|
+
* declared name or the seam's namespace marker `'*'`.
|
|
125
|
+
* @param specifier - the module specifier this binding resolves against.
|
|
126
|
+
* @returns the resolved import binding.
|
|
127
|
+
*/
|
|
128
|
+
function pythonBinding(child, bindsSymbol, specifier) {
|
|
129
|
+
const name = child.type === 'aliased_import' ? fieldText(child, 'name', child.text) : child.text;
|
|
130
|
+
const local = child.type === 'aliased_import' ? fieldText(child, 'alias', name) : name;
|
|
131
|
+
return { localName: local, importedName: bindsSymbol ? name : '*', specifier };
|
|
132
|
+
}
|
|
133
|
+
/** Python import extraction: `import_statement` (bare) and `import_from_statement` (relative-capable). */
|
|
134
|
+
function pythonImports(node) {
|
|
135
|
+
if (node.type === 'import_statement') {
|
|
136
|
+
return namedChildren(node)
|
|
137
|
+
.filter(child => child.type === 'dotted_name' || child.type === 'aliased_import')
|
|
138
|
+
.map((child) => {
|
|
139
|
+
const name = child.type === 'aliased_import' ? fieldText(child, 'name', child.text) : child.text;
|
|
140
|
+
return pythonBinding(child, false, name);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// `module_name` is required by the grammar's `import_from_statement` rule, including the dots-only
|
|
144
|
+
// form (`from . import x`); the null case only satisfies `childForFieldName`'s general return type.
|
|
145
|
+
const moduleNode = node.childForFieldName('module_name');
|
|
146
|
+
/* v8 ignore next */
|
|
147
|
+
const specifier = moduleNode?.text ?? '';
|
|
148
|
+
return namedChildren(node)
|
|
149
|
+
// The module node itself is a named child alongside the imported names — excluded by identity,
|
|
150
|
+
// not by node type, because an absolute module specifier is a `dotted_name` too, indistinguishable
|
|
151
|
+
// by type from an imported symbol written the same way (`from x import y`).
|
|
152
|
+
.filter(child => !(moduleNode !== null && child.equals(moduleNode)))
|
|
153
|
+
.filter(child => child.type === 'dotted_name' || child.type === 'aliased_import' || child.type === 'wildcard_import')
|
|
154
|
+
.flatMap((child) => {
|
|
155
|
+
if (child.type === 'wildcard_import')
|
|
156
|
+
return [];
|
|
157
|
+
return [pythonBinding(child, true, specifier)];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Go import extraction: `import_declaration` wraps one or more `import_spec` nodes, each requiring a `path`. */
|
|
161
|
+
function goImports(node) {
|
|
162
|
+
return descendantsOfType(node, ['import_spec']).map((spec) => {
|
|
163
|
+
// `path` is required by the grammar's `import_spec` rule; the null case only satisfies
|
|
164
|
+
// `childForFieldName`'s general `Node | null` return type.
|
|
165
|
+
const path = spec.childForFieldName('path');
|
|
166
|
+
/* v8 ignore next */
|
|
167
|
+
const raw = path === null ? '' : (namedChildren(path)[0]?.text ?? path.text.slice(1, -1));
|
|
168
|
+
// `String.prototype.split` always returns at least one element, so `.pop()` is never `undefined`;
|
|
169
|
+
// the fallback only satisfies the general array-access return type.
|
|
170
|
+
/* v8 ignore next */
|
|
171
|
+
const local = fieldText(spec, 'name', raw.split('/').pop() ?? raw);
|
|
172
|
+
// A package import binds no individual symbol; resolution never matches on `importedName: '*'`,
|
|
173
|
+
// but recording the binding still lets `status`-adjacent tooling see what a file imports.
|
|
174
|
+
return { localName: local, importedName: '*', specifier: raw };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Extract every definition, call, and import from one parsed file.
|
|
179
|
+
* @param tree - the file's parsed syntax tree.
|
|
180
|
+
* @param spec - the language's extraction table entry.
|
|
181
|
+
* @returns the raw, not-yet-resolved extraction.
|
|
182
|
+
*/
|
|
183
|
+
export function extractFile(tree, spec) {
|
|
184
|
+
const definitions = [];
|
|
185
|
+
const calls = [];
|
|
186
|
+
const imports = [];
|
|
187
|
+
const containerNames = [];
|
|
188
|
+
const containerKeys = [null];
|
|
189
|
+
function visit(node) {
|
|
190
|
+
const rule = matchDefinition(node, spec.definitions);
|
|
191
|
+
if (rule !== undefined) {
|
|
192
|
+
const nameNode = node.childForFieldName(rule.nameField);
|
|
193
|
+
// A matched rule's node type is always the NAMED-declaration form the grammar mandates a name
|
|
194
|
+
// for; the anonymous form (`function_expression`, `class` as an expression, both produced by an
|
|
195
|
+
// anonymous default export) parses as a different node type this rule never matches.
|
|
196
|
+
/* v8 ignore next */
|
|
197
|
+
if (nameNode !== null) {
|
|
198
|
+
const key = `${node.startPosition.row}:${node.startPosition.column}`;
|
|
199
|
+
const parentKey = containerKeys[containerKeys.length - 1] ?? null;
|
|
200
|
+
definitions.push({
|
|
201
|
+
key,
|
|
202
|
+
parentKey,
|
|
203
|
+
kind: rule.kind,
|
|
204
|
+
name: nameNode.text,
|
|
205
|
+
container: [...containerNames],
|
|
206
|
+
startLine: node.startPosition.row + 1,
|
|
207
|
+
endLine: node.endPosition.row + 1,
|
|
208
|
+
startColumn: node.startPosition.column,
|
|
209
|
+
endColumn: node.endPosition.column,
|
|
210
|
+
isExported: isExported(node, spec.language, nameNode.text),
|
|
211
|
+
isAsync: hasKeywordChild(node, 'async'),
|
|
212
|
+
isStatic: hasKeywordChild(node, 'static'),
|
|
213
|
+
});
|
|
214
|
+
containerNames.push(nameNode.text);
|
|
215
|
+
containerKeys.push(key);
|
|
216
|
+
for (const child of namedChildren(node))
|
|
217
|
+
visit(child);
|
|
218
|
+
containerKeys.pop();
|
|
219
|
+
containerNames.pop();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (spec.callTypes.includes(node.type)) {
|
|
224
|
+
// Every call-shaped node type in every language table entry requires its callee field; the null
|
|
225
|
+
// case only satisfies `childForFieldName`'s general return type.
|
|
226
|
+
const callee = node.childForFieldName(spec.callFunctionField);
|
|
227
|
+
/* v8 ignore next */
|
|
228
|
+
const name = callee === null ? undefined : calleeName(callee);
|
|
229
|
+
if (name !== undefined) {
|
|
230
|
+
calls.push({
|
|
231
|
+
callerKey: containerKeys[containerKeys.length - 1] ?? null,
|
|
232
|
+
calleeName: name,
|
|
233
|
+
line: node.startPosition.row + 1,
|
|
234
|
+
column: node.startPosition.column,
|
|
235
|
+
// `callee` is non-null whenever `name` is: the optional chaining only satisfies the type
|
|
236
|
+
// system's view of the field lookup above, not a real possibility here.
|
|
237
|
+
/* v8 ignore next */
|
|
238
|
+
isMemberCall: callee?.type !== 'identifier',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (spec.importTypes.includes(node.type)) {
|
|
243
|
+
imports.push(...extractImports(node, spec.language));
|
|
244
|
+
}
|
|
245
|
+
for (const child of namedChildren(node))
|
|
246
|
+
visit(child);
|
|
247
|
+
}
|
|
248
|
+
visit(tree.rootNode);
|
|
249
|
+
return { definitions, calls, imports };
|
|
250
|
+
}
|
|
251
|
+
/** Dispatch import extraction to the language family that owns `node`'s syntax. */
|
|
252
|
+
function extractImports(node, language) {
|
|
253
|
+
switch (language) {
|
|
254
|
+
case 'typescript':
|
|
255
|
+
case 'tsx':
|
|
256
|
+
case 'javascript':
|
|
257
|
+
case 'jsx':
|
|
258
|
+
return ecmascriptImports(node);
|
|
259
|
+
case 'python':
|
|
260
|
+
return pythonImports(node);
|
|
261
|
+
case 'go':
|
|
262
|
+
return goImports(node);
|
|
263
|
+
/* v8 ignore next 2 -- exhaustive over LANGUAGE_TABLE's current language labels; unreachable. */
|
|
264
|
+
default:
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether a declaration is exported from its module, by the export construct its own language
|
|
270
|
+
* defines: ECMAScript wraps an exported statement in `export_statement`; Go's spec defines an
|
|
271
|
+
* exported identifier as one starting with an uppercase letter, with no separate keyword; Python
|
|
272
|
+
* defines no export construct at all, so every Python declaration reports `false` rather than guess
|
|
273
|
+
* one from a naming convention or an `__all__` list the extractor does not read.
|
|
274
|
+
* @param node - the definition node.
|
|
275
|
+
* @param language - the seam language label the file was parsed as.
|
|
276
|
+
* @param name - the declaration's simple name.
|
|
277
|
+
* @returns whether the language's own export rule marks this declaration exported.
|
|
278
|
+
*/
|
|
279
|
+
function isExported(node, language, name) {
|
|
280
|
+
if (language === 'go')
|
|
281
|
+
return /^\p{Lu}/u.test(name);
|
|
282
|
+
if (language === 'python')
|
|
283
|
+
return false;
|
|
284
|
+
let current = node.parent;
|
|
285
|
+
while (current !== null) {
|
|
286
|
+
if (current.type === 'export_statement')
|
|
287
|
+
return true;
|
|
288
|
+
// A statement block or class body ends the search: an export wraps a top-level statement, never
|
|
289
|
+
// reaches inside a function or class body to a nested declaration.
|
|
290
|
+
if (current.type === 'statement_block' || current.type === 'class_body')
|
|
291
|
+
return false;
|
|
292
|
+
current = current.parent;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hooks as an opt-in alternative to live file watching: a caller who wants a reindex to follow
|
|
3
|
+
* `git checkout`/`merge`/`commit`/`rebase` rather than (or in addition to) every individual file edit
|
|
4
|
+
* can install a hook that runs one command after each.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately just a library function, never wired into plugin load or exposed as a model-visible
|
|
7
|
+
* tool: `.git/hooks/*` is shared, ambient state that outlives this plugin and can collide with a
|
|
8
|
+
* user's other tooling (husky, lint-staged, ...) — installing it automatically, or letting a model
|
|
9
|
+
* decide to install it, is a materially bigger blast radius than the `.codegraph/` cache this package
|
|
10
|
+
* otherwise owns outright. A caller wires this in deliberately, from their own init script.
|
|
11
|
+
*
|
|
12
|
+
* Resolving *which* `hooksDir` to use — including the "worktrees share one hooks directory" case — is
|
|
13
|
+
* the caller's job, composed from {@link detectWorktree} in `worktree.ts`; this module only ever
|
|
14
|
+
* touches the one directory path it is given.
|
|
15
|
+
* @module dsh-plugin-codegraph-tree-sitter/git-hooks
|
|
16
|
+
*/
|
|
17
|
+
/** Marks the start of this package's block inside a hook script. */
|
|
18
|
+
export declare const HOOK_MARKER_BEGIN = "# >>> dsh-plugin-codegraph sync >>>";
|
|
19
|
+
/** Marks the end of this package's block inside a hook script. */
|
|
20
|
+
export declare const HOOK_MARKER_END = "# <<< dsh-plugin-codegraph sync <<<";
|
|
21
|
+
/** Where hooks live, and what to run once installed there. */
|
|
22
|
+
export interface GitHooksOptions {
|
|
23
|
+
/** Absolute path to the git hooks directory (typically `<repo>/.git/hooks`). */
|
|
24
|
+
readonly hooksDir: string;
|
|
25
|
+
/** Shell command to run from each installed hook, verbatim. */
|
|
26
|
+
readonly command: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install `options.command` into every hook in {@link HOOK_NAMES} under `options.hooksDir`.
|
|
30
|
+
*
|
|
31
|
+
* A hook that does not exist yet is created fresh (`#!/bin/sh` plus the marker block) and made
|
|
32
|
+
* executable. A hook that already exists keeps its own content: a prior install's marker block is
|
|
33
|
+
* replaced in place (idempotent re-install, e.g. after `command` changes), otherwise the block is
|
|
34
|
+
* appended after whatever the file already contains — this package never discards another tool's hook.
|
|
35
|
+
* @param options - the hooks directory and the command to install.
|
|
36
|
+
* @returns the hook names now carrying this package's block (whether newly created or pre-existing).
|
|
37
|
+
*/
|
|
38
|
+
export declare function installGitHooks(options: GitHooksOptions): Promise<{
|
|
39
|
+
installed: readonly string[];
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Remove this package's marker block from every hook under `options.hooksDir`, leaving the rest of
|
|
43
|
+
* each file (another tool's hook content, or a user's own) untouched. A hook that does not exist, or
|
|
44
|
+
* exists but was never installed by {@link installGitHooks}, is left alone entirely — this never
|
|
45
|
+
* deletes a hook file outright, even one that becomes empty once the block is removed.
|
|
46
|
+
* @param options - the hooks directory to clean up.
|
|
47
|
+
* @returns the hook names this package's block was actually removed from.
|
|
48
|
+
*/
|
|
49
|
+
export declare function uninstallGitHooks(options: Pick<GitHooksOptions, 'hooksDir'>): Promise<{
|
|
50
|
+
removed: readonly string[];
|
|
51
|
+
}>;
|
|
52
|
+
//# sourceMappingURL=git-hooks.d.ts.map
|
package/lib/git-hooks.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hooks as an opt-in alternative to live file watching: a caller who wants a reindex to follow
|
|
3
|
+
* `git checkout`/`merge`/`commit`/`rebase` rather than (or in addition to) every individual file edit
|
|
4
|
+
* can install a hook that runs one command after each.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately just a library function, never wired into plugin load or exposed as a model-visible
|
|
7
|
+
* tool: `.git/hooks/*` is shared, ambient state that outlives this plugin and can collide with a
|
|
8
|
+
* user's other tooling (husky, lint-staged, ...) — installing it automatically, or letting a model
|
|
9
|
+
* decide to install it, is a materially bigger blast radius than the `.codegraph/` cache this package
|
|
10
|
+
* otherwise owns outright. A caller wires this in deliberately, from their own init script.
|
|
11
|
+
*
|
|
12
|
+
* Resolving *which* `hooksDir` to use — including the "worktrees share one hooks directory" case — is
|
|
13
|
+
* the caller's job, composed from {@link detectWorktree} in `worktree.ts`; this module only ever
|
|
14
|
+
* touches the one directory path it is given.
|
|
15
|
+
* @module dsh-plugin-codegraph-tree-sitter/git-hooks
|
|
16
|
+
*/
|
|
17
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
/** Marks the start of this package's block inside a hook script. */
|
|
20
|
+
export const HOOK_MARKER_BEGIN = '# >>> dsh-plugin-codegraph sync >>>';
|
|
21
|
+
/** Marks the end of this package's block inside a hook script. */
|
|
22
|
+
export const HOOK_MARKER_END = '# <<< dsh-plugin-codegraph sync <<<';
|
|
23
|
+
/** The git hook names this module installs into: every point a working tree can change via git. */
|
|
24
|
+
const HOOK_NAMES = ['post-checkout', 'post-merge', 'post-commit', 'post-rewrite'];
|
|
25
|
+
/** Matches this package's own marker block (and one adjacent blank line) anywhere in a hook script. */
|
|
26
|
+
const MARKER_BLOCK = new RegExp(`\\n?${HOOK_MARKER_BEGIN}[\\s\\S]*?${HOOK_MARKER_END}\\n?`);
|
|
27
|
+
/**
|
|
28
|
+
* Install `options.command` into every hook in {@link HOOK_NAMES} under `options.hooksDir`.
|
|
29
|
+
*
|
|
30
|
+
* A hook that does not exist yet is created fresh (`#!/bin/sh` plus the marker block) and made
|
|
31
|
+
* executable. A hook that already exists keeps its own content: a prior install's marker block is
|
|
32
|
+
* replaced in place (idempotent re-install, e.g. after `command` changes), otherwise the block is
|
|
33
|
+
* appended after whatever the file already contains — this package never discards another tool's hook.
|
|
34
|
+
* @param options - the hooks directory and the command to install.
|
|
35
|
+
* @returns the hook names now carrying this package's block (whether newly created or pre-existing).
|
|
36
|
+
*/
|
|
37
|
+
export async function installGitHooks(options) {
|
|
38
|
+
await mkdir(options.hooksDir, { recursive: true });
|
|
39
|
+
const block = `${HOOK_MARKER_BEGIN}\n${options.command}\n${HOOK_MARKER_END}\n`;
|
|
40
|
+
const installed = [];
|
|
41
|
+
for (const hookName of HOOK_NAMES) {
|
|
42
|
+
const path = join(options.hooksDir, hookName);
|
|
43
|
+
const existing = await readFile(path, 'utf8').catch(() => undefined);
|
|
44
|
+
const content = existing === undefined
|
|
45
|
+
? `#!/bin/sh\n${block}`
|
|
46
|
+
: MARKER_BLOCK.test(existing)
|
|
47
|
+
? existing.replace(MARKER_BLOCK, `\n${block}`)
|
|
48
|
+
: `${existing.endsWith('\n') ? existing : `${existing}\n`}${block}`;
|
|
49
|
+
await writeFile(path, content, 'utf8');
|
|
50
|
+
await chmod(path, 0o755);
|
|
51
|
+
installed.push(hookName);
|
|
52
|
+
}
|
|
53
|
+
return { installed };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove this package's marker block from every hook under `options.hooksDir`, leaving the rest of
|
|
57
|
+
* each file (another tool's hook content, or a user's own) untouched. A hook that does not exist, or
|
|
58
|
+
* exists but was never installed by {@link installGitHooks}, is left alone entirely — this never
|
|
59
|
+
* deletes a hook file outright, even one that becomes empty once the block is removed.
|
|
60
|
+
* @param options - the hooks directory to clean up.
|
|
61
|
+
* @returns the hook names this package's block was actually removed from.
|
|
62
|
+
*/
|
|
63
|
+
export async function uninstallGitHooks(options) {
|
|
64
|
+
const removed = [];
|
|
65
|
+
for (const hookName of HOOK_NAMES) {
|
|
66
|
+
const path = join(options.hooksDir, hookName);
|
|
67
|
+
const existing = await readFile(path, 'utf8').catch(() => undefined);
|
|
68
|
+
if (existing === undefined || !MARKER_BLOCK.test(existing))
|
|
69
|
+
continue;
|
|
70
|
+
await writeFile(path, existing.replace(MARKER_BLOCK, ''), 'utf8');
|
|
71
|
+
removed.push(hookName);
|
|
72
|
+
}
|
|
73
|
+
return { removed };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=git-hooks.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deliberately small subset of `.gitignore` pattern syntax, just enough to keep the indexer's
|
|
3
|
+
* exclusion set in step with what a project's own build tooling already hides from version control —
|
|
4
|
+
* without a new dependency on a full gitignore-matching library.
|
|
5
|
+
*
|
|
6
|
+
* Only the project root's `.gitignore` is read; per-directory `.gitignore` files nested below it are
|
|
7
|
+
* not consulted, matching the walk's own root-relative worldview.
|
|
8
|
+
*
|
|
9
|
+
* Supported:
|
|
10
|
+
* - Blank lines and lines starting with `#` are comments.
|
|
11
|
+
* - A leading `!` negates: a later matching rule re-includes a path an earlier rule excluded.
|
|
12
|
+
* - A pattern containing `/` (leading or internal) is anchored to the project root; one with no `/`
|
|
13
|
+
* at all matches at any depth, by basename.
|
|
14
|
+
* - A trailing `/` restricts the rule to directories.
|
|
15
|
+
* - `*` matches any run of characters other than `/`, within one path segment.
|
|
16
|
+
*
|
|
17
|
+
* Not supported. Each is treated as a literal character (or, for `**`, as two literal `*` globs),
|
|
18
|
+
* which only ever makes a pattern *more* specific than a real gitignore would — it degrades toward
|
|
19
|
+
* "excludes nothing extra" rather than toward "excludes too much":
|
|
20
|
+
* - `**` (explicit multi-segment wildcards — plain unanchored patterns already cross depths)
|
|
21
|
+
* - character classes (`[abc]`), `?`, and backslash escapes
|
|
22
|
+
* - git's rule that a negation cannot resurrect a path inside an already-excluded directory: here,
|
|
23
|
+
* a negation re-includes whatever it matches regardless of ancestor exclusions.
|
|
24
|
+
* @module dsh-plugin-codegraph-tree-sitter/gitignore
|
|
25
|
+
*/
|
|
26
|
+
/** One parsed `.gitignore` rule, pre-compiled to a matcher. */
|
|
27
|
+
export interface GitignoreRule {
|
|
28
|
+
/** Matches a full project-relative, `/`-separated path. */
|
|
29
|
+
readonly pattern: RegExp;
|
|
30
|
+
/** `!`-prefixed: a match re-includes rather than excludes. */
|
|
31
|
+
readonly negated: boolean;
|
|
32
|
+
/** Trailing-`/`: only ever matches a directory. */
|
|
33
|
+
readonly directoryOnly: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse `.gitignore` file content into an ordered rule list.
|
|
37
|
+
* @param content - the raw file text.
|
|
38
|
+
* @returns rules in file order. Matching must apply them in order and keep the last match, per
|
|
39
|
+
* git's own precedence — a later line overrides an earlier one it conflicts with.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseGitignore(content: string): GitignoreRule[];
|
|
42
|
+
/**
|
|
43
|
+
* Whether `relativePath` should be excluded per `rules`. Later rules win over earlier ones, matching
|
|
44
|
+
* git's own precedence: the loop keeps applying matches rather than stopping at the first one.
|
|
45
|
+
* @param rules - parsed rules, in file order.
|
|
46
|
+
* @param relativePath - the path to test, relative to the project root, `/`-separated.
|
|
47
|
+
* @param isDirectory - whether the path names a directory; directory-only rules skip files.
|
|
48
|
+
* @returns whether the path is excluded.
|
|
49
|
+
*/
|
|
50
|
+
export declare function matchesGitignore(rules: readonly GitignoreRule[], relativePath: string, isDirectory: boolean): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Load and parse `<projectRoot>/.gitignore`.
|
|
53
|
+
* @param projectRoot - absolute path of the project root.
|
|
54
|
+
* @returns the parsed rules, or an empty array when there is no root `.gitignore` — a missing file
|
|
55
|
+
* means "nothing extra to exclude," not a failure, so this never throws.
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadGitignore(projectRoot: string): Promise<GitignoreRule[]>;
|
|
58
|
+
//# sourceMappingURL=gitignore.d.ts.map
|