gitnexus 1.6.9-rc.14 → 1.6.9-rc.16
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/core/ingestion/languages/c-cpp.js +5 -0
- package/dist/core/ingestion/languages/csharp.js +3 -0
- package/dist/core/ingestion/languages/dart.js +3 -1
- package/dist/core/ingestion/languages/go.js +7 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin.js +3 -0
- package/dist/core/ingestion/languages/php.js +19 -8
- package/dist/core/ingestion/languages/ruby.js +15 -0
- package/dist/core/ingestion/languages/rust.js +8 -0
- package/dist/core/ingestion/languages/swift.js +3 -0
- package/dist/core/ingestion/languages/typescript/nuxt-auto-imports.d.ts +72 -0
- package/dist/core/ingestion/languages/typescript/nuxt-auto-imports.js +315 -0
- package/dist/core/ingestion/languages/typescript/scope-resolver.js +170 -5
- package/dist/core/ingestion/languages/typescript.js +11 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +81 -2
- package/dist/core/ingestion/utils/ast-helpers.js +142 -0
- package/dist/core/ingestion/workers/parse-worker.js +13 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ const FUNCTION_DECLARATION_TYPES = new Set([
|
|
|
27
27
|
'generator_function_declaration',
|
|
28
28
|
'function_item',
|
|
29
29
|
]);
|
|
30
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
30
31
|
import { createFieldExtractor } from '../field-extractors/generic.js';
|
|
31
32
|
import { cConfig as cFieldConfig, cppConfig as cppFieldConfig, } from '../field-extractors/configs/c-cpp.js';
|
|
32
33
|
import { createMethodExtractor } from '../method-extractors/generic.js';
|
|
@@ -348,6 +349,8 @@ export const cProvider = defineLanguage({
|
|
|
348
349
|
}),
|
|
349
350
|
variableExtractor: createVariableExtractor(cVariableConfig),
|
|
350
351
|
classExtractor: cClassExtractor,
|
|
352
|
+
// ── Doxygen doc comment → description (issue #2270) ──
|
|
353
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
351
354
|
labelOverride: cppLabelOverride,
|
|
352
355
|
builtInNames: C_BUILT_INS,
|
|
353
356
|
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
|
@@ -430,6 +433,8 @@ export const cppProvider = defineLanguage({
|
|
|
430
433
|
}),
|
|
431
434
|
variableExtractor: createVariableExtractor(cppVariableConfig),
|
|
432
435
|
classExtractor: cppClassExtractor,
|
|
436
|
+
// ── Doxygen doc comment → description (issue #2270) ──
|
|
437
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
433
438
|
labelOverride: cppLabelOverride,
|
|
434
439
|
builtInNames: C_BUILT_INS,
|
|
435
440
|
extractTemplateConstraints: extractCppTemplateConstraintsForProvider,
|
|
@@ -14,6 +14,7 @@ import { csharpExportChecker } from '../export-detection.js';
|
|
|
14
14
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
15
15
|
import { csharpImportConfig } from '../import-resolvers/configs/csharp.js';
|
|
16
16
|
import { CSHARP_QUERIES } from '../tree-sitter-queries.js';
|
|
17
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
17
18
|
import { createCallExtractor } from '../call-extractors/generic.js';
|
|
18
19
|
import { csharpCallConfig } from '../call-extractors/configs/csharp.js';
|
|
19
20
|
import { createFieldExtractor } from '../field-extractors/generic.js';
|
|
@@ -180,6 +181,8 @@ export const csharpProvider = defineLanguage({
|
|
|
180
181
|
methodExtractor: createMethodExtractor(csharpMethodConfig),
|
|
181
182
|
variableExtractor: createVariableExtractor(csharpVariableConfig),
|
|
182
183
|
classExtractor: createClassExtractor(csharpClassConfig),
|
|
184
|
+
// ── XML doc comments (`///`) → description (issue #2270) ──
|
|
185
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
183
186
|
builtInNames: BUILT_INS,
|
|
184
187
|
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
|
185
188
|
// C# is the second migration after Python. See ./csharp/index.ts for
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* as a sibling of function_signature/method_signature (not as a child).
|
|
9
9
|
* The hook resolves the enclosing function by inspecting the previous sibling.
|
|
10
10
|
*/
|
|
11
|
-
import { FUNCTION_NODE_TYPES } from '../utils/ast-helpers.js';
|
|
11
|
+
import { createLeadingDocDescriptionExtractor, FUNCTION_NODE_TYPES, } from '../utils/ast-helpers.js';
|
|
12
12
|
import { SupportedLanguages } from '../../../_shared/index.js';
|
|
13
13
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
14
14
|
import { dartClassConfig } from '../class-extractors/configs/dart.js';
|
|
@@ -108,6 +108,8 @@ export const dartProvider = defineLanguage({
|
|
|
108
108
|
methodExtractor: createMethodExtractor(dartMethodConfig),
|
|
109
109
|
variableExtractor: createVariableExtractor(dartVariableConfig),
|
|
110
110
|
classExtractor: createClassExtractor(dartClassConfig),
|
|
111
|
+
// ── Dartdoc (`///`) → description (issue #2270) ──
|
|
112
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
111
113
|
enclosingFunctionFinder: dartEnclosingFunctionFinder,
|
|
112
114
|
builtInNames: DART_BUILT_INS,
|
|
113
115
|
// ── Scope-based resolution hooks (RFC #909 Ring 3, issue #939) ──────────────
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { SupportedLanguages } from '../../../_shared/index.js';
|
|
11
11
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
12
12
|
import { goClassConfig } from '../class-extractors/configs/go.js';
|
|
13
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
13
14
|
import { createGoCfgVisitor } from '../cfg/visitors/go.js';
|
|
14
15
|
import { defineLanguage } from '../language-provider.js';
|
|
15
16
|
import { typeConfig as goConfig } from '../type-extractors/go.js';
|
|
@@ -126,6 +127,12 @@ export const goProvider = defineLanguage({
|
|
|
126
127
|
methodExtractor: createMethodExtractor(goMethodConfig),
|
|
127
128
|
variableExtractor: createVariableExtractor(goVariableConfig),
|
|
128
129
|
classExtractor: createClassExtractor(goClassConfig),
|
|
130
|
+
// ── godoc (`//` leading comments) → description (issue #2270). Build/tool
|
|
131
|
+
// directives (//go:…, // +build, //nolint, //line) are not documentation. ──
|
|
132
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor({
|
|
133
|
+
lineCommentPrefixes: ['//'],
|
|
134
|
+
lineDirectivePrefixes: ['//go:', '// +build', '//nolint', '//line'],
|
|
135
|
+
}),
|
|
129
136
|
builtInNames: GO_BUILT_INS,
|
|
130
137
|
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
|
131
138
|
emitScopeCaptures: emitGoScopeCaptures,
|
|
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { javaClassConfig } from '../class-extractors/configs/jvm.js';
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
13
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
14
15
|
import { extractSpringRoutes } from '../route-extractors/spring.js';
|
|
15
16
|
import { javaExportChecker } from '../export-detection.js';
|
|
@@ -94,6 +95,8 @@ export const javaProvider = defineLanguage({
|
|
|
94
95
|
methodExtractor: createMethodExtractor(javaMethodConfig),
|
|
95
96
|
variableExtractor: createVariableExtractor(javaVariableConfig),
|
|
96
97
|
classExtractor: createClassExtractor(javaClassConfig),
|
|
98
|
+
// ── Javadoc → description (issue #2270) ──
|
|
99
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
97
100
|
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
|
98
101
|
emitScopeCaptures: emitJavaScopeCaptures,
|
|
99
102
|
// ── PDG: per-function CFG + def/use harvest (#2195 U4) ──
|
|
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { kotlinClassConfig } from '../class-extractors/configs/jvm.js';
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
13
14
|
import { assertCloneable } from '../workers/clone-safety.js';
|
|
14
15
|
import { kotlinTypeConfig } from '../type-extractors/jvm.js';
|
|
15
16
|
import { kotlinExportChecker } from '../export-detection.js';
|
|
@@ -153,6 +154,8 @@ export const kotlinProvider = defineLanguage({
|
|
|
153
154
|
variableExtractor: createVariableExtractor(kotlinVariableConfig),
|
|
154
155
|
classExtractor: createClassExtractor(kotlinClassConfig),
|
|
155
156
|
builtInNames: BUILT_INS,
|
|
157
|
+
// ── KDoc → description (issue #2270) ──
|
|
158
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
156
159
|
labelOverride: (functionNode, defaultLabel) => {
|
|
157
160
|
if (defaultLabel !== 'Function')
|
|
158
161
|
return defaultLabel;
|
|
@@ -10,13 +10,13 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { phpClassConfig } from '../class-extractors/configs/php.js';
|
|
12
12
|
import { createPhpCfgVisitor } from '../cfg/visitors/php.js';
|
|
13
|
-
import { defineLanguage } from '../language-provider.js';
|
|
13
|
+
import { defineLanguage, } from '../language-provider.js';
|
|
14
14
|
import { typeConfig as phpConfig } from '../type-extractors/php.js';
|
|
15
15
|
import { phpExportChecker } from '../export-detection.js';
|
|
16
16
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
17
17
|
import { phpImportConfig } from '../import-resolvers/configs/php.js';
|
|
18
18
|
import { PHP_QUERIES } from '../tree-sitter-queries.js';
|
|
19
|
-
import { findDescendant, extractStringContent } from '../utils/ast-helpers.js';
|
|
19
|
+
import { findDescendant, extractStringContent, createLeadingDocDescriptionExtractor, } from '../utils/ast-helpers.js';
|
|
20
20
|
import { createFieldExtractor } from '../field-extractors/generic.js';
|
|
21
21
|
import { phpConfig as phpFieldConfig } from '../field-extractors/configs/php.js';
|
|
22
22
|
import { createMethodExtractor } from '../method-extractors/generic.js';
|
|
@@ -203,18 +203,29 @@ function extractEloquentRelationDescription(methodNode) {
|
|
|
203
203
|
return relType;
|
|
204
204
|
return null;
|
|
205
205
|
}
|
|
206
|
+
/** PHPDoc-docblock fallback, shared with the other leading-comment languages. */
|
|
207
|
+
const phpLeadingDocFallback = createLeadingDocDescriptionExtractor();
|
|
206
208
|
/**
|
|
207
209
|
* LanguageProvider.descriptionExtractor implementation for PHP.
|
|
208
|
-
*
|
|
210
|
+
* Eloquent model property metadata and relationship descriptions take
|
|
211
|
+
* precedence (they are richer than prose); otherwise documentable symbols fall
|
|
212
|
+
* back to their leading PHPDoc docblock (issue #2270), mirroring the other
|
|
213
|
+
* leading-comment languages.
|
|
209
214
|
*/
|
|
210
215
|
function phpDescriptionExtractor(nodeLabel, nodeName, captureMap) {
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
const propertyNode = captureMap['definition.property'];
|
|
217
|
+
if (nodeLabel === 'Property' && propertyNode) {
|
|
218
|
+
const eloquentProperty = extractPhpPropertyDescription(nodeName, propertyNode);
|
|
219
|
+
if (eloquentProperty)
|
|
220
|
+
return eloquentProperty;
|
|
213
221
|
}
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const methodNode = captureMap['definition.method'];
|
|
223
|
+
if (nodeLabel === 'Method' && methodNode) {
|
|
224
|
+
const eloquentRelation = extractEloquentRelationDescription(methodNode);
|
|
225
|
+
if (eloquentRelation)
|
|
226
|
+
return eloquentRelation;
|
|
216
227
|
}
|
|
217
|
-
return
|
|
228
|
+
return phpLeadingDocFallback(nodeLabel, nodeName, captureMap);
|
|
218
229
|
}
|
|
219
230
|
/** Detect Laravel route files by path convention. */
|
|
220
231
|
function isPhpRouteFile(filePath) {
|
|
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { rubyClassConfig } from '../class-extractors/configs/ruby.js';
|
|
12
12
|
import { defineLanguage } from '../language-provider.js';
|
|
13
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
13
14
|
import { typeConfig as rubyConfig } from '../type-extractors/ruby.js';
|
|
14
15
|
import { routeRubyCall } from '../call-routing.js';
|
|
15
16
|
import { rubyExportChecker } from '../export-detection.js';
|
|
@@ -179,6 +180,20 @@ export const rubyProvider = defineLanguage({
|
|
|
179
180
|
}),
|
|
180
181
|
variableExtractor: createVariableExtractor(rubyVariableConfig),
|
|
181
182
|
classExtractor: createClassExtractor(rubyClassConfig),
|
|
183
|
+
// ── Leading `#` comments (RDoc/YARD) → description (issue #2270). Magic
|
|
184
|
+
// comments and the shebang are not documentation. ──
|
|
185
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor({
|
|
186
|
+
lineCommentPrefixes: ['#'],
|
|
187
|
+
lineDirectivePrefixes: [
|
|
188
|
+
'# frozen_string_literal:',
|
|
189
|
+
'# encoding:',
|
|
190
|
+
'# coding:',
|
|
191
|
+
'# -*-',
|
|
192
|
+
'#!',
|
|
193
|
+
'# rubocop:',
|
|
194
|
+
'# typed:',
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
182
197
|
labelOverride: rubyLabelOverride,
|
|
183
198
|
// Ruby MRO is kind-aware: prepend providers beat the class's own method,
|
|
184
199
|
// which in turn beats include providers. The graph-level MRO phase
|
|
@@ -11,6 +11,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
11
11
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
12
12
|
import { rustClassConfig } from '../class-extractors/configs/rust.js';
|
|
13
13
|
import { defineLanguage } from '../language-provider.js';
|
|
14
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
15
|
import { typeConfig as rustConfig } from '../type-extractors/rust.js';
|
|
15
16
|
import { rustExportChecker } from '../export-detection.js';
|
|
16
17
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
@@ -159,6 +160,13 @@ export const rustProvider = defineLanguage({
|
|
|
159
160
|
}),
|
|
160
161
|
variableExtractor: createVariableExtractor(rustVariableConfig),
|
|
161
162
|
classExtractor: createClassExtractor(rustClassConfig),
|
|
163
|
+
// ── Rust outer doc comments (`///`, `/** */`) → description (issue #2270).
|
|
164
|
+
// `//!` / `/*!` are INNER docs (document the enclosing item), so they must
|
|
165
|
+
// not attach to the following item — opt out of both. ──
|
|
166
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor({
|
|
167
|
+
lineCommentPrefixes: ['///'],
|
|
168
|
+
blockDocPrefixes: ['/**'],
|
|
169
|
+
}),
|
|
162
170
|
builtInNames: BUILT_INS,
|
|
163
171
|
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
|
164
172
|
emitScopeCaptures: emitRustScopeCaptures,
|
|
@@ -13,6 +13,7 @@ import { swiftExportChecker } from '../export-detection.js';
|
|
|
13
13
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
14
14
|
import { swiftImportConfig } from '../import-resolvers/configs/swift.js';
|
|
15
15
|
import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
|
|
16
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
16
17
|
import { createFieldExtractor } from '../field-extractors/generic.js';
|
|
17
18
|
import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
|
|
18
19
|
import { createMethodExtractor } from '../method-extractors/generic.js';
|
|
@@ -222,6 +223,8 @@ export const swiftProvider = defineLanguage({
|
|
|
222
223
|
}),
|
|
223
224
|
variableExtractor: createVariableExtractor(swiftVariableConfig),
|
|
224
225
|
classExtractor: createClassExtractor(swiftClassConfig),
|
|
226
|
+
// ── Swift doc comments (`///`, `/** */`) → description (issue #2270) ──
|
|
227
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
225
228
|
orderSameNameTypeCandidates: orderSwiftSameNameTypeCandidates,
|
|
226
229
|
builtInNames: BUILT_INS,
|
|
227
230
|
// ── Scope-based resolution hooks (RFC #909 Ring 3, issue #937). See
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt v4 auto-import resolution for the TypeScript scope-resolver.
|
|
3
|
+
*
|
|
4
|
+
* Nuxt and Nitro both make symbols available project-wide without explicit
|
|
5
|
+
* import statements. Two sources cover the common cases:
|
|
6
|
+
*
|
|
7
|
+
* 1. `.nuxt/imports.d.ts` - client/shared composables generated by Nuxt at
|
|
8
|
+
* build time. Only entries whose source is a project-local relative path
|
|
9
|
+
* (i.e. NOT a package in node_modules or a Nuxt runtime alias) are
|
|
10
|
+
* indexed. These are project utils and composables the developer wrote.
|
|
11
|
+
*
|
|
12
|
+
* 2. `server/utils/**` - Nitro auto-imports all exports from this directory
|
|
13
|
+
* tree into every `server/api/`, `server/routes/`, and
|
|
14
|
+
* `server/middleware/` file. These are not included in `imports.d.ts`
|
|
15
|
+
* because they are server-only.
|
|
16
|
+
*
|
|
17
|
+
* The two sources are kept in separate maps because their visibility scopes
|
|
18
|
+
* differ: app/client files do not see `server/utils`, while Nitro server
|
|
19
|
+
* handlers prefer same-named server utilities over client composables.
|
|
20
|
+
*
|
|
21
|
+
* Detection: if `.nuxt/imports.d.ts` is absent the repo is not a Nuxt project
|
|
22
|
+
* and this module returns null immediately, adding zero overhead to non-Nuxt
|
|
23
|
+
* analysis runs. The `server/utils` scan is only attempted when the Nuxt
|
|
24
|
+
* detection succeeds, so it also costs nothing for non-Nuxt repos.
|
|
25
|
+
*
|
|
26
|
+
* Limitations:
|
|
27
|
+
* - Auto-import edges are still heuristic rather than type-checked. The
|
|
28
|
+
* emitter tags them with confidence 0.75.
|
|
29
|
+
* - Re-exports with non-matching aliases (e.g. `flatUnwrap as unwrapSlot`
|
|
30
|
+
* where `flatUnwrap` is the graph node name) try both the original export
|
|
31
|
+
* name and the local alias when looking up the graph node.
|
|
32
|
+
* - Only direct exports are indexed; barrel re-exports that chain through
|
|
33
|
+
* multiple files are not followed.
|
|
34
|
+
*/
|
|
35
|
+
export type NuxtAutoImportScope = 'client' | 'server';
|
|
36
|
+
/** A single auto-imported symbol and where it lives in the repo. */
|
|
37
|
+
export interface NuxtAutoImportEntry {
|
|
38
|
+
/** The name used at call sites (the alias when `export { X as Y }` form). */
|
|
39
|
+
readonly localName: string;
|
|
40
|
+
/** The original export name in the source file (before `as`). */
|
|
41
|
+
readonly exportName: string;
|
|
42
|
+
/** Repo-relative POSIX path to the source file (with extension). */
|
|
43
|
+
readonly sourceFile: string;
|
|
44
|
+
/** Which Nuxt/Nitro visibility scope supplied this entry. */
|
|
45
|
+
readonly scope: NuxtAutoImportScope;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Aggregated auto-import maps for one Nuxt workspace, keyed by the local
|
|
49
|
+
* name used in calling code. Client/shared entries come from
|
|
50
|
+
* `.nuxt/imports.d.ts`; server entries come from `server/utils`.
|
|
51
|
+
*/
|
|
52
|
+
export interface NuxtAutoImportConfig {
|
|
53
|
+
readonly clientByLocalName: ReadonlyMap<string, NuxtAutoImportEntry>;
|
|
54
|
+
readonly serverByLocalName: ReadonlyMap<string, NuxtAutoImportEntry>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load the Nuxt auto-import map for `repoRoot`.
|
|
58
|
+
*
|
|
59
|
+
* Returns null when:
|
|
60
|
+
* - `.nuxt/imports.d.ts` does not exist (non-Nuxt project or pre-build), or
|
|
61
|
+
* - the file exists but yields zero project-local entries and `server/utils`
|
|
62
|
+
* is absent or empty.
|
|
63
|
+
*
|
|
64
|
+
* The `server/utils` scan is only attempted when `.nuxt/imports.d.ts` was
|
|
65
|
+
* successfully read, confirming this is an initialized Nuxt project. This
|
|
66
|
+
* avoids partial results from repos that have a `server/utils` directory but
|
|
67
|
+
* are not Nuxt projects.
|
|
68
|
+
*/
|
|
69
|
+
export declare function loadNuxtAutoImports(repoRoot: string): Promise<NuxtAutoImportConfig | null>;
|
|
70
|
+
export declare function hasNuxtAutoImports(config: NuxtAutoImportConfig): boolean;
|
|
71
|
+
export declare function getNuxtAutoImportEntry(config: NuxtAutoImportConfig, localName: string, callerFile: string): NuxtAutoImportEntry | undefined;
|
|
72
|
+
export declare function isNitroServerRuntimeFile(filePath: string): boolean;
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt v4 auto-import resolution for the TypeScript scope-resolver.
|
|
3
|
+
*
|
|
4
|
+
* Nuxt and Nitro both make symbols available project-wide without explicit
|
|
5
|
+
* import statements. Two sources cover the common cases:
|
|
6
|
+
*
|
|
7
|
+
* 1. `.nuxt/imports.d.ts` - client/shared composables generated by Nuxt at
|
|
8
|
+
* build time. Only entries whose source is a project-local relative path
|
|
9
|
+
* (i.e. NOT a package in node_modules or a Nuxt runtime alias) are
|
|
10
|
+
* indexed. These are project utils and composables the developer wrote.
|
|
11
|
+
*
|
|
12
|
+
* 2. `server/utils/**` - Nitro auto-imports all exports from this directory
|
|
13
|
+
* tree into every `server/api/`, `server/routes/`, and
|
|
14
|
+
* `server/middleware/` file. These are not included in `imports.d.ts`
|
|
15
|
+
* because they are server-only.
|
|
16
|
+
*
|
|
17
|
+
* The two sources are kept in separate maps because their visibility scopes
|
|
18
|
+
* differ: app/client files do not see `server/utils`, while Nitro server
|
|
19
|
+
* handlers prefer same-named server utilities over client composables.
|
|
20
|
+
*
|
|
21
|
+
* Detection: if `.nuxt/imports.d.ts` is absent the repo is not a Nuxt project
|
|
22
|
+
* and this module returns null immediately, adding zero overhead to non-Nuxt
|
|
23
|
+
* analysis runs. The `server/utils` scan is only attempted when the Nuxt
|
|
24
|
+
* detection succeeds, so it also costs nothing for non-Nuxt repos.
|
|
25
|
+
*
|
|
26
|
+
* Limitations:
|
|
27
|
+
* - Auto-import edges are still heuristic rather than type-checked. The
|
|
28
|
+
* emitter tags them with confidence 0.75.
|
|
29
|
+
* - Re-exports with non-matching aliases (e.g. `flatUnwrap as unwrapSlot`
|
|
30
|
+
* where `flatUnwrap` is the graph node name) try both the original export
|
|
31
|
+
* name and the local alias when looking up the graph node.
|
|
32
|
+
* - Only direct exports are indexed; barrel re-exports that chain through
|
|
33
|
+
* multiple files are not followed.
|
|
34
|
+
*/
|
|
35
|
+
import fs from 'fs/promises';
|
|
36
|
+
import path from 'path';
|
|
37
|
+
const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
38
|
+
const GENERATED_DIR_NAMES = new Set([
|
|
39
|
+
'.git',
|
|
40
|
+
'.nuxt',
|
|
41
|
+
'.output',
|
|
42
|
+
'.next',
|
|
43
|
+
'build',
|
|
44
|
+
'coverage',
|
|
45
|
+
'dist',
|
|
46
|
+
'node_modules',
|
|
47
|
+
]);
|
|
48
|
+
const IMPORTS_DTS_EXPORT_RE = /^export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm;
|
|
49
|
+
const NITRO_DECLARATION_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)|^export\s+(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/gm;
|
|
50
|
+
const NITRO_VARIABLE_EXPORT_RE = /^export\s+(?:const|let|var)\s+([^;\n]+)/gm;
|
|
51
|
+
// Matches the binding name at the head of a single declarator (`name`, `name:`, `name =`).
|
|
52
|
+
// A leading `{`/`[` (destructuring) does not match, so destructured exports are skipped.
|
|
53
|
+
const DECLARATOR_NAME_RE = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)/;
|
|
54
|
+
// ---- loader -----------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* Load the Nuxt auto-import map for `repoRoot`.
|
|
57
|
+
*
|
|
58
|
+
* Returns null when:
|
|
59
|
+
* - `.nuxt/imports.d.ts` does not exist (non-Nuxt project or pre-build), or
|
|
60
|
+
* - the file exists but yields zero project-local entries and `server/utils`
|
|
61
|
+
* is absent or empty.
|
|
62
|
+
*
|
|
63
|
+
* The `server/utils` scan is only attempted when `.nuxt/imports.d.ts` was
|
|
64
|
+
* successfully read, confirming this is an initialized Nuxt project. This
|
|
65
|
+
* avoids partial results from repos that have a `server/utils` directory but
|
|
66
|
+
* are not Nuxt projects.
|
|
67
|
+
*/
|
|
68
|
+
export async function loadNuxtAutoImports(repoRoot) {
|
|
69
|
+
const clientByLocalName = new Map();
|
|
70
|
+
const serverByLocalName = new Map();
|
|
71
|
+
const nuxtInitialized = await collectImportsDts(repoRoot, clientByLocalName);
|
|
72
|
+
// Only scan server/utils when imports.d.ts was present, confirming this is
|
|
73
|
+
// an initialized Nuxt project. Without this gate, a non-Nuxt repo with a
|
|
74
|
+
// server/utils directory would get spurious Nitro auto-import edges.
|
|
75
|
+
if (nuxtInitialized) {
|
|
76
|
+
await collectNitroServerUtils(repoRoot, serverByLocalName);
|
|
77
|
+
}
|
|
78
|
+
const config = { clientByLocalName, serverByLocalName };
|
|
79
|
+
return hasNuxtAutoImports(config) ? config : null;
|
|
80
|
+
}
|
|
81
|
+
export function hasNuxtAutoImports(config) {
|
|
82
|
+
return config.clientByLocalName.size > 0 || config.serverByLocalName.size > 0;
|
|
83
|
+
}
|
|
84
|
+
export function getNuxtAutoImportEntry(config, localName, callerFile) {
|
|
85
|
+
if (isNitroServerRuntimeFile(callerFile)) {
|
|
86
|
+
// Nitro auto-imports only `server/utils/**` into the server context; app
|
|
87
|
+
// `composables/` are Vue-app-only. Server callers therefore resolve the
|
|
88
|
+
// server map only — no client fallback, which would emit cross-context
|
|
89
|
+
// CALLS edges Nitro never actually creates.
|
|
90
|
+
return config.serverByLocalName.get(localName);
|
|
91
|
+
}
|
|
92
|
+
return config.clientByLocalName.get(localName);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Directories whose files run in the Nitro server context and so receive
|
|
96
|
+
* `server/utils` auto-imports without an explicit import: API routes, server
|
|
97
|
+
* routes, middleware, plugins, and (Nitro 2.6+) tasks.
|
|
98
|
+
*/
|
|
99
|
+
const NITRO_RUNTIME_DIR_PREFIXES = [
|
|
100
|
+
'server/api/',
|
|
101
|
+
'server/routes/',
|
|
102
|
+
'server/middleware/',
|
|
103
|
+
'server/plugins/',
|
|
104
|
+
'server/tasks/',
|
|
105
|
+
];
|
|
106
|
+
export function isNitroServerRuntimeFile(filePath) {
|
|
107
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
108
|
+
return NITRO_RUNTIME_DIR_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
109
|
+
}
|
|
110
|
+
// ---- .nuxt/imports.d.ts -----------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Parse `.nuxt/imports.d.ts` and add project-local entries to `byLocalName`.
|
|
113
|
+
*
|
|
114
|
+
* The file contains lines of the form:
|
|
115
|
+
* export { name1, name2, origName as alias } from 'source'
|
|
116
|
+
*
|
|
117
|
+
* Only entries with a source that is a project-local relative path
|
|
118
|
+
* (starts with `./` or `../` AND does not contain `node_modules`) are
|
|
119
|
+
* included. Nuxt runtime paths (`#app/...`) and third-party packages are
|
|
120
|
+
* intentionally skipped because they have no graph nodes in the repo.
|
|
121
|
+
*
|
|
122
|
+
* @returns true when `.nuxt/imports.d.ts` was successfully read.
|
|
123
|
+
*/
|
|
124
|
+
async function collectImportsDts(repoRoot, byLocalName) {
|
|
125
|
+
const importsPath = path.join(repoRoot, '.nuxt', 'imports.d.ts');
|
|
126
|
+
let content;
|
|
127
|
+
try {
|
|
128
|
+
content = await fs.readFile(importsPath, 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const nuxtDir = path.join(repoRoot, '.nuxt');
|
|
134
|
+
IMPORTS_DTS_EXPORT_RE.lastIndex = 0;
|
|
135
|
+
let m;
|
|
136
|
+
while ((m = IMPORTS_DTS_EXPORT_RE.exec(content)) !== null) {
|
|
137
|
+
const symbolsRaw = m[1];
|
|
138
|
+
const source = m[2];
|
|
139
|
+
if (!isProjectLocalPath(source))
|
|
140
|
+
continue;
|
|
141
|
+
// Containment guard: a crafted source (`from '../../../../etc/passwd'`)
|
|
142
|
+
// passes the relative-path check but escapes the repo. Skip anything that
|
|
143
|
+
// resolves outside repoRoot before touching the filesystem.
|
|
144
|
+
const resolvedBase = path.resolve(nuxtDir, source);
|
|
145
|
+
if (!isWithinRepo(repoRoot, resolvedBase))
|
|
146
|
+
continue;
|
|
147
|
+
const resolvedFile = await resolveExtension(resolvedBase);
|
|
148
|
+
if (resolvedFile === null)
|
|
149
|
+
continue;
|
|
150
|
+
const sourceFile = toRepoPosix(repoRoot, resolvedFile);
|
|
151
|
+
for (const raw of symbolsRaw.split(',')) {
|
|
152
|
+
const trimmed = raw.trim();
|
|
153
|
+
if (!trimmed)
|
|
154
|
+
continue;
|
|
155
|
+
const asIdx = trimmed.indexOf(' as ');
|
|
156
|
+
const exportName = asIdx >= 0 ? trimmed.slice(0, asIdx).trim() : trimmed;
|
|
157
|
+
const localName = asIdx >= 0 ? trimmed.slice(asIdx + 4).trim() : trimmed;
|
|
158
|
+
if (!localName || !exportName)
|
|
159
|
+
continue;
|
|
160
|
+
if (!byLocalName.has(localName)) {
|
|
161
|
+
byLocalName.set(localName, { localName, exportName, sourceFile, scope: 'client' });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// ---- server/utils (Nitro auto-imports) --------------------------------------
|
|
168
|
+
/**
|
|
169
|
+
* Scan `server/utils/**` and register every exported symbol as a Nitro
|
|
170
|
+
* auto-import. Nitro makes all exports from this directory tree available
|
|
171
|
+
* without an import statement in server routes and middleware.
|
|
172
|
+
*/
|
|
173
|
+
async function collectNitroServerUtils(repoRoot, byLocalName) {
|
|
174
|
+
const serverUtilsDir = path.join(repoRoot, 'server', 'utils');
|
|
175
|
+
if (!(await dirExists(serverUtilsDir)))
|
|
176
|
+
return;
|
|
177
|
+
const tsFiles = await collectTsFiles(serverUtilsDir);
|
|
178
|
+
for (const absPath of tsFiles) {
|
|
179
|
+
let content;
|
|
180
|
+
try {
|
|
181
|
+
content = await fs.readFile(absPath, 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const sourceFile = toRepoPosix(repoRoot, absPath);
|
|
187
|
+
for (const name of extractNitroExportNames(content)) {
|
|
188
|
+
if (!byLocalName.has(name)) {
|
|
189
|
+
byLocalName.set(name, { localName: name, exportName: name, sourceFile, scope: 'server' });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ---- helpers ----------------------------------------------------------------
|
|
195
|
+
function isProjectLocalPath(source) {
|
|
196
|
+
if (!source.startsWith('./') && !source.startsWith('../'))
|
|
197
|
+
return false;
|
|
198
|
+
if (source.includes('node_modules'))
|
|
199
|
+
return false;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/** True when `absPath` is `repoRoot` itself or lives beneath it. */
|
|
203
|
+
function isWithinRepo(repoRoot, absPath) {
|
|
204
|
+
const root = path.resolve(repoRoot);
|
|
205
|
+
return absPath === root || absPath.startsWith(root + path.sep);
|
|
206
|
+
}
|
|
207
|
+
async function resolveExtension(base) {
|
|
208
|
+
const directFile = await firstExistingFile([...FILE_EXTENSIONS.map((ext) => base + ext), base]);
|
|
209
|
+
if (directFile !== null)
|
|
210
|
+
return directFile;
|
|
211
|
+
return firstExistingFile(FILE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)));
|
|
212
|
+
}
|
|
213
|
+
async function firstExistingFile(candidates) {
|
|
214
|
+
const matches = await Promise.all(candidates.map(async (candidate) => ((await isFile(candidate)) ? candidate : null)));
|
|
215
|
+
return matches.find((candidate) => candidate !== null) ?? null;
|
|
216
|
+
}
|
|
217
|
+
async function isFile(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
const stat = await fs.stat(filePath);
|
|
220
|
+
return stat.isFile();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function toRepoPosix(repoRoot, absPath) {
|
|
227
|
+
return path.relative(repoRoot, absPath).replace(/\\/g, '/');
|
|
228
|
+
}
|
|
229
|
+
async function dirExists(dirPath) {
|
|
230
|
+
try {
|
|
231
|
+
const stat = await fs.stat(dirPath);
|
|
232
|
+
return stat.isDirectory();
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Recursively collect supported TypeScript/JavaScript files under a directory. */
|
|
239
|
+
async function collectTsFiles(dir) {
|
|
240
|
+
const results = [];
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return results;
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.isDirectory() && GENERATED_DIR_NAMES.has(entry.name))
|
|
250
|
+
continue;
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
results.push(...(await collectTsFiles(full)));
|
|
254
|
+
}
|
|
255
|
+
else if (entry.isFile() && hasSupportedSourceExtension(entry.name)) {
|
|
256
|
+
results.push(full);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
function hasSupportedSourceExtension(fileName) {
|
|
262
|
+
return FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
263
|
+
}
|
|
264
|
+
function extractNitroExportNames(content) {
|
|
265
|
+
const names = new Set();
|
|
266
|
+
NITRO_DECLARATION_EXPORT_RE.lastIndex = 0;
|
|
267
|
+
let declaration;
|
|
268
|
+
while ((declaration = NITRO_DECLARATION_EXPORT_RE.exec(content)) !== null) {
|
|
269
|
+
const name = declaration[1] ?? declaration[2];
|
|
270
|
+
if (name)
|
|
271
|
+
names.add(name);
|
|
272
|
+
}
|
|
273
|
+
NITRO_VARIABLE_EXPORT_RE.lastIndex = 0;
|
|
274
|
+
let variableDeclaration;
|
|
275
|
+
while ((variableDeclaration = NITRO_VARIABLE_EXPORT_RE.exec(content)) !== null) {
|
|
276
|
+
// Only the LHS binding name of each top-level declarator is a Nitro export.
|
|
277
|
+
// Splitting on top-level commas and reading the leading identifier avoids
|
|
278
|
+
// capturing RHS tokens (arrow params, object keys, operands) as export names,
|
|
279
|
+
// and tolerates commas inside generic type annotations (`x: Map<a, b> = …`).
|
|
280
|
+
for (const declarator of splitTopLevelDeclarators(variableDeclaration[1])) {
|
|
281
|
+
const name = DECLARATOR_NAME_RE.exec(declarator);
|
|
282
|
+
if (name)
|
|
283
|
+
names.add(name[1]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return [...names];
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Split a `const`/`let`/`var` declarator list on top-level commas, tracking
|
|
290
|
+
* `()`, `[]`, `{}`, and `<>` nesting so commas inside call args, object/array
|
|
291
|
+
* literals, and generic type arguments do not split a declarator. Errs toward
|
|
292
|
+
* under-splitting on pathological RHS (a missed binding name, never a spurious
|
|
293
|
+
* one) — Nitro only auto-imports real top-level binding names.
|
|
294
|
+
*/
|
|
295
|
+
function splitTopLevelDeclarators(text) {
|
|
296
|
+
const parts = [];
|
|
297
|
+
let depth = 0;
|
|
298
|
+
let start = 0;
|
|
299
|
+
for (let i = 0; i < text.length; i++) {
|
|
300
|
+
const ch = text[i];
|
|
301
|
+
if (ch === '(' || ch === '[' || ch === '{' || ch === '<') {
|
|
302
|
+
depth++;
|
|
303
|
+
}
|
|
304
|
+
else if (ch === ')' || ch === ']' || ch === '}' || ch === '>') {
|
|
305
|
+
if (depth > 0)
|
|
306
|
+
depth--;
|
|
307
|
+
}
|
|
308
|
+
else if (ch === ',' && depth === 0) {
|
|
309
|
+
parts.push(text.slice(start, i));
|
|
310
|
+
start = i + 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
parts.push(text.slice(start));
|
|
314
|
+
return parts;
|
|
315
|
+
}
|
|
@@ -12,12 +12,24 @@
|
|
|
12
12
|
* ./query.ts (TYPESCRIPT_SCOPE_QUERY constant).
|
|
13
13
|
*/
|
|
14
14
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
15
|
+
import { generateId } from '../../../../lib/utils.js';
|
|
15
16
|
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
|
16
17
|
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
|
18
|
+
import { simpleKey } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|
17
19
|
import { typescriptProvider } from '../typescript.js';
|
|
18
20
|
import { loadTsconfigPaths } from '../../language-config.js';
|
|
19
21
|
import { buildSuffixIndex } from '../../import-resolvers/utils.js';
|
|
20
22
|
import { typescriptArityCompatibility, typescriptMergeBindings, resolveTsTarget, } from './index.js';
|
|
23
|
+
import { getNuxtAutoImportEntry, hasNuxtAutoImports, loadNuxtAutoImports, } from './nuxt-auto-imports.js';
|
|
24
|
+
const TYPESCRIPT_TYPE_ONLY_BINDING_TYPES = new Set([
|
|
25
|
+
'Interface',
|
|
26
|
+
'Type',
|
|
27
|
+
'TypeAlias',
|
|
28
|
+
'Typedef',
|
|
29
|
+
'Trait',
|
|
30
|
+
'Annotation',
|
|
31
|
+
'Decorator',
|
|
32
|
+
]);
|
|
21
33
|
/**
|
|
22
34
|
* Build a `resolveImportTarget` adapter that memoizes the workspace
|
|
23
35
|
* file list, the lower-cased file list, and the per-pass `resolveCache`
|
|
@@ -65,11 +77,14 @@ const typescriptScopeResolver = {
|
|
|
65
77
|
importEdgeReason: 'typescript-scope: import',
|
|
66
78
|
resolveImportTarget: makeTsResolveImportTarget(),
|
|
67
79
|
// Threaded into `resolveImportTarget` so tsconfig path aliases
|
|
68
|
-
// (`@/services/user`, `~/x`,
|
|
80
|
+
// (`@/services/user`, `~/x`, ...) resolve through the same standard
|
|
69
81
|
// resolver branch the legacy DAG uses. One I/O round-trip per
|
|
70
82
|
// workspace pass; the orchestrator awaits this once.
|
|
83
|
+
// `nuxtAutoImports` is null for non-Nuxt projects (no .nuxt/imports.d.ts),
|
|
84
|
+
// so this adds zero overhead to ordinary TypeScript repos.
|
|
71
85
|
loadResolutionConfig: async (repoPath) => ({
|
|
72
86
|
tsconfigPaths: await loadTsconfigPaths(repoPath),
|
|
87
|
+
nuxtAutoImports: await loadNuxtAutoImports(repoPath),
|
|
73
88
|
}),
|
|
74
89
|
// TypeScript declaration merging + LEGB: local > import > wildcard,
|
|
75
90
|
// separated by declaration space (value / type / namespace). The
|
|
@@ -95,23 +110,173 @@ const typescriptScopeResolver = {
|
|
|
95
110
|
fieldFallbackOnMethodLookup: false,
|
|
96
111
|
propagatesReturnTypesAcrossImports: true,
|
|
97
112
|
// TypeScript uses `.values()` / `.keys()` method-call syntax for
|
|
98
|
-
// collection views
|
|
113
|
+
// collection views -- no property-style accessors like C#'s
|
|
99
114
|
// `Dictionary<K,V>.Values`. Leave `unwrapCollectionAccessor`
|
|
100
115
|
// undefined and let the regular member-call branch handle them.
|
|
101
116
|
//
|
|
102
|
-
// `collapseMemberCallsByCallerTarget` left undefined (= false)
|
|
117
|
+
// `collapseMemberCallsByCallerTarget` left undefined (= false) --
|
|
103
118
|
// TypeScript legacy DAG emits one edge per call site, so
|
|
104
119
|
// per-site dedup is the parity target.
|
|
105
120
|
//
|
|
106
|
-
// `populateNamespaceSiblings` left undefined
|
|
121
|
+
// `populateNamespaceSiblings` left undefined -- TypeScript requires
|
|
107
122
|
// an explicit `import` / namespace augmentation for cross-file
|
|
108
123
|
// visibility; there's no implicit same-namespace sibling rule
|
|
109
124
|
// like C#'s.
|
|
110
125
|
//
|
|
111
|
-
// `hoistTypeBindingsToModule`
|
|
126
|
+
// `hoistTypeBindingsToModule` -- `tsBindingScopeFor` DOES hoist
|
|
112
127
|
// method return-type bindings to the enclosing Module scope
|
|
113
128
|
// (mirrors C#), so enable the walk-up that lets the compound-
|
|
114
129
|
// receiver resolver find them.
|
|
115
130
|
hoistTypeBindingsToModule: true,
|
|
131
|
+
/**
|
|
132
|
+
* Emit CALLS edges for Nuxt/Nitro auto-imported symbols that are used
|
|
133
|
+
* without an explicit import statement.
|
|
134
|
+
*
|
|
135
|
+
* Nuxt makes composables and server utils available project-wide via its
|
|
136
|
+
* auto-import system. Because no `import` statement exists, the standard
|
|
137
|
+
* scope-resolution passes cannot create call-graph edges for these symbols.
|
|
138
|
+
* This hook recovers those edges after all normal resolution has run.
|
|
139
|
+
*
|
|
140
|
+
* For each TypeScript file the hook:
|
|
141
|
+
* 1. Builds the set of files already explicitly imported (to avoid
|
|
142
|
+
* creating duplicate edges for symbols imported conventionally).
|
|
143
|
+
* 2. Iterates parsed free-call reference sites and checks each against the
|
|
144
|
+
* auto-import map selected for the caller's Nuxt/Nitro scope.
|
|
145
|
+
* 3. For each hit that is not shadowed by a local binding or explicit
|
|
146
|
+
* import, emits a CALLS edge from the file's File node to the target
|
|
147
|
+
* function node, and an IMPORTS edge from the caller file to the source
|
|
148
|
+
* file (once per pair).
|
|
149
|
+
*
|
|
150
|
+
* Confidence is 0.75 (below the 0.9 used for fully resolved edges) to
|
|
151
|
+
* signal that these edges are heuristic rather than type-checked.
|
|
152
|
+
*/
|
|
153
|
+
emitPostResolutionEdges(graph, parsedFiles, nodeLookup, indexes, ctx) {
|
|
154
|
+
const cfg = ctx.resolutionConfig;
|
|
155
|
+
const autoImports = cfg?.nuxtAutoImports;
|
|
156
|
+
if (!autoImports || !hasNuxtAutoImports(autoImports))
|
|
157
|
+
return;
|
|
158
|
+
// Pre-build a file -> explicit imported local names index so importing one
|
|
159
|
+
// symbol from a source does not suppress other auto-imported symbols from it.
|
|
160
|
+
const explicitImportNamesByFile = new Map();
|
|
161
|
+
for (const [scopeId, edges] of indexes.imports) {
|
|
162
|
+
const scope = indexes.scopeTree.getScope(scopeId);
|
|
163
|
+
if (!scope?.filePath)
|
|
164
|
+
continue;
|
|
165
|
+
let names = explicitImportNamesByFile.get(scope.filePath);
|
|
166
|
+
if (!names) {
|
|
167
|
+
names = new Set();
|
|
168
|
+
explicitImportNamesByFile.set(scope.filePath, names);
|
|
169
|
+
}
|
|
170
|
+
for (const edge of edges) {
|
|
171
|
+
// Record the local name whether or not the import resolved to a file.
|
|
172
|
+
// An explicit import of a name — even from an unresolved external
|
|
173
|
+
// package (`import { useAuto } from '@vueuse/core'`) — is authoritative
|
|
174
|
+
// shadowing intent and must suppress the auto-import for that name.
|
|
175
|
+
if (edge.localName)
|
|
176
|
+
names.add(edge.localName);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const parsedFile of parsedFiles) {
|
|
180
|
+
const { filePath } = parsedFile;
|
|
181
|
+
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx'))
|
|
182
|
+
continue;
|
|
183
|
+
const fileId = generateId('File', filePath);
|
|
184
|
+
const explicitImports = explicitImportNamesByFile.get(filePath) ?? new Set();
|
|
185
|
+
// Track (sourceFile) pairs already handled for this caller to avoid
|
|
186
|
+
// emitting duplicate IMPORTS edges and duplicate CALLS edges per symbol.
|
|
187
|
+
const emittedImports = new Set();
|
|
188
|
+
const emittedCalls = new Set();
|
|
189
|
+
for (const site of parsedFile.referenceSites) {
|
|
190
|
+
if (site.kind !== 'call' || site.callForm !== 'free')
|
|
191
|
+
continue;
|
|
192
|
+
const localName = site.name;
|
|
193
|
+
const entry = getNuxtAutoImportEntry(autoImports, localName, filePath);
|
|
194
|
+
if (!entry)
|
|
195
|
+
continue;
|
|
196
|
+
const { exportName, sourceFile } = entry;
|
|
197
|
+
// Skip when the file already binds this name explicitly, when the file
|
|
198
|
+
// IS the source, or when a lexical same-file binding shadows it.
|
|
199
|
+
if (explicitImports.has(localName) ||
|
|
200
|
+
sourceFile === filePath ||
|
|
201
|
+
hasLocalBindingInScopeChain(site.inScope, localName, filePath, indexes)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Emit one IMPORTS edge per (caller, sourceFile) pair.
|
|
205
|
+
if (!emittedImports.has(sourceFile)) {
|
|
206
|
+
emittedImports.add(sourceFile);
|
|
207
|
+
const targetFileId = generateId('File', sourceFile);
|
|
208
|
+
if (graph.getNode(targetFileId)) {
|
|
209
|
+
graph.addRelationship({
|
|
210
|
+
id: generateId('IMPORTS', `${fileId}->nuxt-auto-import->${targetFileId}`),
|
|
211
|
+
sourceId: fileId,
|
|
212
|
+
targetId: targetFileId,
|
|
213
|
+
type: 'IMPORTS',
|
|
214
|
+
confidence: 0.75,
|
|
215
|
+
reason: 'nuxt-auto-import-file',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Emit one CALLS edge per (caller, symbol) pair.
|
|
220
|
+
const callKey = `${sourceFile}::${localName}`;
|
|
221
|
+
if (emittedCalls.has(callKey))
|
|
222
|
+
continue;
|
|
223
|
+
emittedCalls.add(callKey);
|
|
224
|
+
// Look up the graph node by export name first, fall back to local name.
|
|
225
|
+
// The fallback handles `default as X` where the function is named X.
|
|
226
|
+
const targetNodeId = nodeLookup.get(simpleKey(sourceFile, exportName)) ??
|
|
227
|
+
(exportName !== localName ? nodeLookup.get(simpleKey(sourceFile, localName)) : undefined);
|
|
228
|
+
if (!targetNodeId || !graph.getNode(targetNodeId))
|
|
229
|
+
continue;
|
|
230
|
+
graph.addRelationship({
|
|
231
|
+
id: generateId('CALLS', `${fileId}:nuxt-auto-import:${localName}->${targetNodeId}`),
|
|
232
|
+
sourceId: fileId,
|
|
233
|
+
targetId: targetNodeId,
|
|
234
|
+
type: 'CALLS',
|
|
235
|
+
confidence: 0.75,
|
|
236
|
+
reason: 'nuxt-auto-import',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
116
241
|
};
|
|
242
|
+
function occupiesTypeScriptValueSpace(type) {
|
|
243
|
+
return !TYPESCRIPT_TYPE_ONLY_BINDING_TYPES.has(type);
|
|
244
|
+
}
|
|
245
|
+
function hasLocalBindingInScopeChain(scopeId, name, filePath, indexes) {
|
|
246
|
+
const visited = new Set();
|
|
247
|
+
let cursor = scopeId;
|
|
248
|
+
while (cursor !== null && cursor !== undefined && !visited.has(cursor)) {
|
|
249
|
+
visited.add(cursor);
|
|
250
|
+
const scope = indexes.scopeTree.getScope(cursor);
|
|
251
|
+
if (!scope)
|
|
252
|
+
return false;
|
|
253
|
+
const localBindings = scope.bindings.get(name);
|
|
254
|
+
if (localBindings?.some((binding) => binding.origin === 'local' &&
|
|
255
|
+
binding.def.filePath === filePath &&
|
|
256
|
+
occupiesTypeScriptValueSpace(binding.def.type))) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
// Type-annotated function parameters — and other value-space type facts
|
|
260
|
+
// such as `self` and variable annotations — live in `scope.typeBindings`,
|
|
261
|
+
// not `scope.bindings`. A parameter named like a composable genuinely
|
|
262
|
+
// shadows the auto-import, and typeBindings never holds a pure type that
|
|
263
|
+
// belongs to callable space, so a same-file presence check here cannot
|
|
264
|
+
// over-suppress a legitimate auto-import.
|
|
265
|
+
//
|
|
266
|
+
// Residual (known limitation): this catches parameters whose annotation the
|
|
267
|
+
// TS scope query records as a type-binding (`p: Named`, generics, unions,
|
|
268
|
+
// predefined, arrays). Function-typed params (`p: () => void`), untyped
|
|
269
|
+
// params, destructured locals (`const { x } = …`), and catch-clause vars
|
|
270
|
+
// are captured by NEITHER map — the scope query emits no `@declaration` /
|
|
271
|
+
// `@type-binding` for them — so those shadow forms still leak an edge.
|
|
272
|
+
// Closing that needs shared TS scope-query/extractor changes that alter call
|
|
273
|
+
// resolution beyond Nuxt, so it is deferred to a follow-up rather than fixed
|
|
274
|
+
// here.
|
|
275
|
+
if (scope.filePath === filePath && scope.typeBindings.has(name)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
cursor = scope.parent;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
117
282
|
export { typescriptScopeResolver };
|
|
@@ -9,6 +9,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
|
|
|
9
9
|
import { defineLanguage } from '../language-provider.js';
|
|
10
10
|
import { createClassExtractor } from '../class-extractors/generic.js';
|
|
11
11
|
import { typescriptClassConfig, javascriptClassConfig, } from '../class-extractors/configs/typescript-javascript.js';
|
|
12
|
+
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
12
13
|
import { createTypeScriptCfgVisitor } from '../cfg/visitors/typescript.js';
|
|
13
14
|
import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js';
|
|
14
15
|
import { tsExportChecker } from '../export-detection.js';
|
|
@@ -287,6 +288,11 @@ export const typescriptProvider = defineLanguage({
|
|
|
287
288
|
}),
|
|
288
289
|
variableExtractor: createVariableExtractor(typescriptVariableConfig),
|
|
289
290
|
classExtractor: createClassExtractor(typescriptClassConfig),
|
|
291
|
+
// ── JSDoc → description (issue #2270). An exported decl is captured as the
|
|
292
|
+
// inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
|
|
293
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor({
|
|
294
|
+
wrapperNodeTypes: ['export_statement'],
|
|
295
|
+
}),
|
|
290
296
|
builtInNames: BUILT_INS,
|
|
291
297
|
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
|
292
298
|
// TypeScript is the third migration after Python and C#. See
|
|
@@ -347,6 +353,11 @@ export const javascriptProvider = defineLanguage({
|
|
|
347
353
|
}),
|
|
348
354
|
variableExtractor: createVariableExtractor(javascriptVariableConfig),
|
|
349
355
|
classExtractor: createClassExtractor(javascriptClassConfig),
|
|
356
|
+
// ── JSDoc → description (issue #2270). An exported decl is captured as the
|
|
357
|
+
// inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
|
|
358
|
+
descriptionExtractor: createLeadingDocDescriptionExtractor({
|
|
359
|
+
wrapperNodeTypes: ['export_statement'],
|
|
360
|
+
}),
|
|
350
361
|
builtInNames: BUILT_INS,
|
|
351
362
|
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
|
352
363
|
// JavaScript is the fourth migration after Python, C#, and TypeScript.
|
|
@@ -34,7 +34,7 @@ export declare const isQualifiableScopeLabel: (nodeLabel: string) => boolean;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare const DEFINITION_CAPTURE_KEYS: readonly ["definition.function", "definition.class", "definition.interface", "definition.method", "definition.struct", "definition.enum", "definition.namespace", "definition.module", "definition.trait", "definition.impl", "definition.type", "definition.const", "definition.static", "definition.variable", "definition.typedef", "definition.macro", "definition.union", "definition.property", "definition.record", "definition.delegate", "definition.annotation", "definition.constructor", "definition.template"];
|
|
36
36
|
/** Extract the definition node from a tree-sitter query capture map. */
|
|
37
|
-
export declare const getDefinitionNodeFromCaptures: (captureMap: Record<string, SyntaxNode>) => SyntaxNode | null;
|
|
37
|
+
export declare const getDefinitionNodeFromCaptures: (captureMap: Record<string, SyntaxNode | undefined>) => SyntaxNode | null;
|
|
38
38
|
type QueryMatchLike = {
|
|
39
39
|
captures: Array<{
|
|
40
40
|
name: string;
|
|
@@ -87,7 +87,7 @@ export declare function findAncestorBeforeBoundary(node: SyntaxNode, targetTypes
|
|
|
87
87
|
* (e.g. C/C++ duplicate skipping, Kotlin Method promotion).
|
|
88
88
|
* Returns null if the capture should be skipped (import, call, C/C++ duplicate, missing name).
|
|
89
89
|
*/
|
|
90
|
-
export declare function getLabelFromCaptures(captureMap: Record<string, SyntaxNode>, provider: LanguageProvider): NodeLabel | null;
|
|
90
|
+
export declare function getLabelFromCaptures(captureMap: Record<string, SyntaxNode | undefined>, provider: LanguageProvider): NodeLabel | null;
|
|
91
91
|
/** Enclosing class info: both the generated node ID and the bare class name. */
|
|
92
92
|
export interface EnclosingClassInfo {
|
|
93
93
|
classId: string;
|
|
@@ -215,6 +215,85 @@ export declare function findDescendant(root: SyntaxNode, type: string): SyntaxNo
|
|
|
215
215
|
export declare function extractStringContent(node: SyntaxNode | null | undefined): string | null;
|
|
216
216
|
/** Find the first direct named child of a tree-sitter node matching the given type. */
|
|
217
217
|
export declare function findChild(node: SyntaxNode, type: string): SyntaxNode | null;
|
|
218
|
+
/**
|
|
219
|
+
* Extract the normalized text of a leading doc comment immediately preceding a
|
|
220
|
+
* definition node — covering both block doc comments (Javadoc / KDoc / JSDoc /
|
|
221
|
+
* PHPDoc / Doxygen, opened by `/**` or `/*!`) and runs of line doc comments
|
|
222
|
+
* (`///`, `//!`, or the caller-supplied prefixes such as Go's `//` or Ruby's
|
|
223
|
+
* `#`). Returns `undefined` when there is no preceding doc comment or it is
|
|
224
|
+
* empty.
|
|
225
|
+
*
|
|
226
|
+
* Grammar-agnostic by design: matches on the comment text prefix rather than a
|
|
227
|
+
* grammar node type, because the comment node is named differently across
|
|
228
|
+
* grammars (`block_comment`, `multiline_comment`, `comment`, `line_comment`).
|
|
229
|
+
* Annotations and modifiers live inside the definition node, so the doc comment
|
|
230
|
+
* remains the definition's `previousNamedSibling` even on annotated/decorated
|
|
231
|
+
* declarations.
|
|
232
|
+
*
|
|
233
|
+
* Block comments are taken as the immediately-preceding sibling (intervening
|
|
234
|
+
* package/import/code siblings already shield a file-level license block from
|
|
235
|
+
* the first declaration). Line doc comments enforce row-adjacency: the first
|
|
236
|
+
* comment must sit on the line directly above the definition, and each comment
|
|
237
|
+
* walked further up must sit directly above the previous one — so a run stops
|
|
238
|
+
* at a blank line. This matches godoc/RDoc/rustdoc convention and prevents an
|
|
239
|
+
* unrelated comment block (a license header, a Ruby shebang + magic comment)
|
|
240
|
+
* separated by a blank line from being absorbed. Adjacency is checked on
|
|
241
|
+
* `startPosition.row` (reliable) rather than `endPosition.row`, since some
|
|
242
|
+
* grammars fold the trailing newline into the comment node.
|
|
243
|
+
*
|
|
244
|
+
* Normalization mirrors Python docstring handling: strip the comment delimiters
|
|
245
|
+
* / per-line markers, then collapse whitespace to single spaces so tag content
|
|
246
|
+
* (`@param`, `@deprecated since 2.0, use computeBalanceV2`) survives.
|
|
247
|
+
*
|
|
248
|
+
* When the captured definition is an inner node and its own preceding sibling
|
|
249
|
+
* carries no doc, the search retries from a wrapping node whose type is listed in
|
|
250
|
+
* `opts.wrapperNodeTypes` (e.g. an `export_statement` wrapping an exported
|
|
251
|
+
* function/class — the JSDoc precedes the wrapper, not the inner declaration).
|
|
252
|
+
*/
|
|
253
|
+
export interface LeadingDocCommentOptions {
|
|
254
|
+
/** Line-comment doc prefixes (defaults to {@link DEFAULT_LINE_DOC_PREFIXES};
|
|
255
|
+
* Go passes `['//']`, Ruby passes `['#']`). */
|
|
256
|
+
lineCommentPrefixes?: readonly string[];
|
|
257
|
+
/** Grammar node types that wrap a definition such that the doc comment is the
|
|
258
|
+
* wrapper's preceding sibling rather than the definition's. TS/JS pass
|
|
259
|
+
* `['export_statement']`. Empty by default → no wrapper retry. */
|
|
260
|
+
wrapperNodeTypes?: readonly string[];
|
|
261
|
+
/** Line-comment prefixes that are tool/build directives or magic comments
|
|
262
|
+
* rather than documentation (Go passes `['//go:', '// +build', …]`, Ruby
|
|
263
|
+
* passes `['# frozen_string_literal:', '#!', …]`). A matching line is skipped
|
|
264
|
+
* in the doc run rather than absorbed. Empty by default. */
|
|
265
|
+
lineDirectivePrefixes?: readonly string[];
|
|
266
|
+
/** Block-comment doc openers (defaults to `['/**', '/*!']`). Rust passes
|
|
267
|
+
* `['/**']` so its inner-doc `/*!` does not attach to the following item. */
|
|
268
|
+
blockDocPrefixes?: readonly string[];
|
|
269
|
+
}
|
|
270
|
+
export declare function extractLeadingDocComment(node: SyntaxNode, opts?: LeadingDocCommentOptions): string | undefined;
|
|
271
|
+
/** Node labels that can carry a leading doc comment — callables and type-like
|
|
272
|
+
* declarations. Field/property/variable/const doc is intentionally excluded
|
|
273
|
+
* (issue #2270 scopes this to method/type documentation). Language-neutral:
|
|
274
|
+
* a label a given grammar never emits simply never matches.
|
|
275
|
+
*
|
|
276
|
+
* Bounded to labels that are also in `embeddings/types.ts` `EMBEDDABLE_LABELS`:
|
|
277
|
+
* the description is only useful once it reaches the embedding metadata header,
|
|
278
|
+
* and the embedding pipeline only queries embeddable labels. Extracting docs
|
|
279
|
+
* for a non-embeddable label is a wasted write that never becomes searchable.
|
|
280
|
+
* A subset invariant in the unit tests guards against drift. Making currently-
|
|
281
|
+
* non-embeddable doc-bearing labels (Module, Delegate, Annotation, and C++
|
|
282
|
+
* `Template`) searchable is tracked as a follow-up — it needs an embedding-
|
|
283
|
+
* pipeline/schema change beyond this fix. */
|
|
284
|
+
export declare const DOC_BEARING_LABELS: ReadonlySet<NodeLabel>;
|
|
285
|
+
/**
|
|
286
|
+
* Build a `LanguageProvider.descriptionExtractor` that surfaces a definition's
|
|
287
|
+
* leading doc comment as its `description` (issue #2270). For labels in
|
|
288
|
+
* {@link DOC_BEARING_LABELS} (which is bounded to embeddable labels) the text
|
|
289
|
+
* then reaches the embedding metadata header and becomes semantically searchable.
|
|
290
|
+
*
|
|
291
|
+
* Language-neutral factory (names no language): guards on
|
|
292
|
+
* {@link DOC_BEARING_LABELS}; callers pass per-language doc-comment behavior via
|
|
293
|
+
* {@link LeadingDocCommentOptions} (line prefixes, export-style wrappers, …)
|
|
294
|
+
* which is threaded straight through to {@link extractLeadingDocComment}.
|
|
295
|
+
*/
|
|
296
|
+
export declare const createLeadingDocDescriptionExtractor: (opts?: LeadingDocCommentOptions) => ((nodeLabel: NodeLabel, nodeName: string, captureMap: Record<string, SyntaxNode | undefined>) => string | undefined);
|
|
218
297
|
/** Convert a tree-sitter node to a `Capture` with 1-based line numbers
|
|
219
298
|
* (matching RFC §2.1). The tag includes the leading `@`. */
|
|
220
299
|
export declare function nodeToCapture(name: string, node: SyntaxNode): Capture;
|
|
@@ -824,6 +824,148 @@ export function findChild(node, type) {
|
|
|
824
824
|
}
|
|
825
825
|
return null;
|
|
826
826
|
}
|
|
827
|
+
/** Remove bidi-override and zero-width control characters. Doc text is
|
|
828
|
+
* attacker-influenced (any indexed repo) and is returned verbatim to MCP
|
|
829
|
+
* clients, so strip Trojan-Source-style hidden controls from the description
|
|
830
|
+
* before it leaves the extractor (#2286 review). Scoped to the doc-comment path
|
|
831
|
+
* only — global `sanitizeUTF8` is intentionally untouched. */
|
|
832
|
+
const stripBidiAndZeroWidth = (text) => Array.from(text)
|
|
833
|
+
.filter((ch) => {
|
|
834
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
835
|
+
// Bidi overrides/isolates (U+202A–202E, U+2066–2069), zero-width
|
|
836
|
+
// space/joiners (U+200B–200D), and BOM/zero-width-no-break (U+FEFF).
|
|
837
|
+
return !((c >= 0x202a && c <= 0x202e) ||
|
|
838
|
+
(c >= 0x2066 && c <= 0x2069) ||
|
|
839
|
+
(c >= 0x200b && c <= 0x200d) ||
|
|
840
|
+
c === 0xfeff);
|
|
841
|
+
})
|
|
842
|
+
.join('');
|
|
843
|
+
/** Normalize a block doc comment body: strip the opening (double-star or
|
|
844
|
+
* bang) delimiter, the closing delimiter, and per-line gutter stars, then
|
|
845
|
+
* collapse whitespace so tag content stays as searchable words. */
|
|
846
|
+
const normalizeBlockDocComment = (text) => {
|
|
847
|
+
const inner = stripBidiAndZeroWidth(text
|
|
848
|
+
.replace(/^\/\*[*!]/, '')
|
|
849
|
+
// Close delimiter: tolerate the degenerate empty comment `/**/`, where the
|
|
850
|
+
// opening strip already consumed the shared `*`, leaving a lone `/`.
|
|
851
|
+
.replace(/\*?\/\s*$/, '')
|
|
852
|
+
.replace(/^[ \t]*\*[ \t]?/gm, ' ')
|
|
853
|
+
.replace(/\s+/g, ' ')
|
|
854
|
+
.trim());
|
|
855
|
+
return inner.length > 0 ? inner : undefined;
|
|
856
|
+
};
|
|
857
|
+
/** Default line-comment prefixes treated as documentation: the universal
|
|
858
|
+
* triple-slash / bang-slash doc markers (Rust, C#, Dart, Swift, Doxygen).
|
|
859
|
+
* Go (`//`) and Ruby (`#`) opt into their conventional markers explicitly. */
|
|
860
|
+
const DEFAULT_LINE_DOC_PREFIXES = ['///', '//!'];
|
|
861
|
+
/** Default block-comment doc openers: Javadoc/JSDoc-style `/**` and Doxygen
|
|
862
|
+
* `/*!`. Rust opts out of `/*!` (and `//!`) because those are *inner* docs that
|
|
863
|
+
* document the enclosing item, not the following one. */
|
|
864
|
+
const DEFAULT_BLOCK_DOC_PREFIXES = ['/**', '/*!'];
|
|
865
|
+
/** A file-top `/** … *\/` license/copyright/file-overview block has no
|
|
866
|
+
* package/import sibling to shield it, so it would otherwise be absorbed as the
|
|
867
|
+
* first declaration's description (PR #2286 review). These markers identify such
|
|
868
|
+
* headers; they are specific enough not to fire on an ordinary symbol doc that
|
|
869
|
+
* merely mentions the word "copyright". `@file`/`@fileoverview` are explicitly
|
|
870
|
+
* file-level JSDoc tags, so a block carrying them is not a symbol doc. */
|
|
871
|
+
const FILE_HEADER_MARKER = /SPDX-License-Identifier|@licen[sc]e\b|@fileoverview\b|@file\b|Licen[sc]ed under|copyright\s*(\(c\)|©|\d{4})/i;
|
|
872
|
+
export function extractLeadingDocComment(node, opts = {}) {
|
|
873
|
+
const lineCommentPrefixes = opts.lineCommentPrefixes ?? DEFAULT_LINE_DOC_PREFIXES;
|
|
874
|
+
const wrapperNodeTypes = opts.wrapperNodeTypes ?? [];
|
|
875
|
+
const lineDirectivePrefixes = opts.lineDirectivePrefixes ?? [];
|
|
876
|
+
const blockDocPrefixes = opts.blockDocPrefixes ?? DEFAULT_BLOCK_DOC_PREFIXES;
|
|
877
|
+
const fromNode = (anchor) => {
|
|
878
|
+
const prev = anchor.previousNamedSibling;
|
|
879
|
+
if (!prev)
|
|
880
|
+
return undefined;
|
|
881
|
+
// Block doc comment: /** ... */ or /*! ... */
|
|
882
|
+
if (blockDocPrefixes.some((p) => prev.text.startsWith(p))) {
|
|
883
|
+
// Skip a file-top license/copyright/overview header (no package/import
|
|
884
|
+
// sibling shields it from the first declaration). A strict row-adjacency
|
|
885
|
+
// check is unreliable here — some grammars fold the trailing newline into
|
|
886
|
+
// the comment node — so match header markers instead.
|
|
887
|
+
if (FILE_HEADER_MARKER.test(prev.text))
|
|
888
|
+
return undefined;
|
|
889
|
+
return normalizeBlockDocComment(prev.text);
|
|
890
|
+
}
|
|
891
|
+
// Run of row-adjacent preceding line doc comments (e.g. `///` or `//`).
|
|
892
|
+
const matchedPrefix = (text) => lineCommentPrefixes.find((prefix) => text.trimStart().startsWith(prefix));
|
|
893
|
+
const isDirective = (text) => lineDirectivePrefixes.some((prefix) => text.trimStart().startsWith(prefix));
|
|
894
|
+
const lines = [];
|
|
895
|
+
let current = prev;
|
|
896
|
+
let expectedRow = anchor.startPosition.row - 1;
|
|
897
|
+
while (current) {
|
|
898
|
+
const text = current.text;
|
|
899
|
+
const prefix = matchedPrefix(text);
|
|
900
|
+
if (prefix === undefined || current.startPosition.row !== expectedRow)
|
|
901
|
+
break;
|
|
902
|
+
// A build/tool directive or magic comment (e.g. `//go:build`,
|
|
903
|
+
// `# frozen_string_literal:`) is not documentation: skip it but keep
|
|
904
|
+
// walking the adjacent run, so a real doc above it is still collected.
|
|
905
|
+
if (!isDirective(text))
|
|
906
|
+
lines.unshift(text.trimStart().slice(prefix.length));
|
|
907
|
+
expectedRow = current.startPosition.row - 1;
|
|
908
|
+
current = current.previousNamedSibling;
|
|
909
|
+
}
|
|
910
|
+
const joined = stripBidiAndZeroWidth(lines.join(' ').replace(/\s+/g, ' ').trim());
|
|
911
|
+
return joined.length > 0 ? joined : undefined;
|
|
912
|
+
};
|
|
913
|
+
const direct = fromNode(node);
|
|
914
|
+
if (direct !== undefined)
|
|
915
|
+
return direct;
|
|
916
|
+
const parent = node.parent;
|
|
917
|
+
if (parent && wrapperNodeTypes.includes(parent.type)) {
|
|
918
|
+
return fromNode(parent);
|
|
919
|
+
}
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
/** Node labels that can carry a leading doc comment — callables and type-like
|
|
923
|
+
* declarations. Field/property/variable/const doc is intentionally excluded
|
|
924
|
+
* (issue #2270 scopes this to method/type documentation). Language-neutral:
|
|
925
|
+
* a label a given grammar never emits simply never matches.
|
|
926
|
+
*
|
|
927
|
+
* Bounded to labels that are also in `embeddings/types.ts` `EMBEDDABLE_LABELS`:
|
|
928
|
+
* the description is only useful once it reaches the embedding metadata header,
|
|
929
|
+
* and the embedding pipeline only queries embeddable labels. Extracting docs
|
|
930
|
+
* for a non-embeddable label is a wasted write that never becomes searchable.
|
|
931
|
+
* A subset invariant in the unit tests guards against drift. Making currently-
|
|
932
|
+
* non-embeddable doc-bearing labels (Module, Delegate, Annotation, and C++
|
|
933
|
+
* `Template`) searchable is tracked as a follow-up — it needs an embedding-
|
|
934
|
+
* pipeline/schema change beyond this fix. */
|
|
935
|
+
export const DOC_BEARING_LABELS = new Set([
|
|
936
|
+
'Function',
|
|
937
|
+
'Method',
|
|
938
|
+
'Constructor',
|
|
939
|
+
'Class',
|
|
940
|
+
'Interface',
|
|
941
|
+
'Enum',
|
|
942
|
+
'Struct',
|
|
943
|
+
'Trait',
|
|
944
|
+
'Record',
|
|
945
|
+
'Union',
|
|
946
|
+
'Namespace',
|
|
947
|
+
'TypeAlias',
|
|
948
|
+
'Macro',
|
|
949
|
+
]);
|
|
950
|
+
/**
|
|
951
|
+
* Build a `LanguageProvider.descriptionExtractor` that surfaces a definition's
|
|
952
|
+
* leading doc comment as its `description` (issue #2270). For labels in
|
|
953
|
+
* {@link DOC_BEARING_LABELS} (which is bounded to embeddable labels) the text
|
|
954
|
+
* then reaches the embedding metadata header and becomes semantically searchable.
|
|
955
|
+
*
|
|
956
|
+
* Language-neutral factory (names no language): guards on
|
|
957
|
+
* {@link DOC_BEARING_LABELS}; callers pass per-language doc-comment behavior via
|
|
958
|
+
* {@link LeadingDocCommentOptions} (line prefixes, export-style wrappers, …)
|
|
959
|
+
* which is threaded straight through to {@link extractLeadingDocComment}.
|
|
960
|
+
*/
|
|
961
|
+
export const createLeadingDocDescriptionExtractor = (opts = {}) => {
|
|
962
|
+
return (nodeLabel, _nodeName, captureMap) => {
|
|
963
|
+
if (!DOC_BEARING_LABELS.has(nodeLabel))
|
|
964
|
+
return undefined;
|
|
965
|
+
const definitionNode = getDefinitionNodeFromCaptures(captureMap);
|
|
966
|
+
return definitionNode ? extractLeadingDocComment(definitionNode, opts) : undefined;
|
|
967
|
+
};
|
|
968
|
+
};
|
|
827
969
|
// ============================================================================
|
|
828
970
|
// Capture + range helpers (formerly python/ast-utils.ts — language-agnostic)
|
|
829
971
|
// ============================================================================
|
|
@@ -1589,7 +1589,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1589
1589
|
}
|
|
1590
1590
|
}
|
|
1591
1591
|
const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${classTemplateTag}${arityTag}${parameterShapeTag}${constraintsTag}`);
|
|
1592
|
-
|
|
1592
|
+
let description;
|
|
1593
|
+
try {
|
|
1594
|
+
description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
|
|
1595
|
+
}
|
|
1596
|
+
catch (err) {
|
|
1597
|
+
// A throw here (an unexpected tree-sitter node shape, a provider bug) must
|
|
1598
|
+
// NOT propagate — it would escape processFileGroup to the language-group
|
|
1599
|
+
// catch, which treats any throw as "parser unavailable" and silently drops
|
|
1600
|
+
// every remaining file in the group. Mirrors the extractTemplateConstraints
|
|
1601
|
+
// guard above (#2286 review).
|
|
1602
|
+
reportWarning(`Description extraction failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1603
|
+
description = undefined;
|
|
1604
|
+
}
|
|
1593
1605
|
let frameworkHint = definitionNode
|
|
1594
1606
|
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
|
1595
1607
|
: null;
|
package/package.json
CHANGED