gitnexus 1.6.11-rc.21 → 1.6.11-rc.23
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/README.md +24 -21
- package/dist/_shared/graph/types.d.ts +6 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +4 -0
- package/dist/cli/ai-context.js +5 -2
- package/dist/cli/analyze-config.js +14 -0
- package/dist/cli/analyze-options.d.ts +5 -0
- package/dist/cli/analyze.js +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/watch.js +2 -0
- package/dist/core/group/cross-impact.d.ts +3 -0
- package/dist/core/group/cross-impact.js +26 -4
- package/dist/core/group/extractors/http-patterns/java.js +18 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
- package/dist/core/group/normalization.d.ts +38 -1
- package/dist/core/group/normalization.js +59 -3
- package/dist/core/group/service.d.ts +11 -0
- package/dist/core/group/service.js +8 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +15 -3
- package/dist/core/group/types.d.ts +13 -0
- package/dist/core/ingestion/frameworks/spring/actuator-runtime.d.ts +26 -0
- package/dist/core/ingestion/frameworks/spring/actuator-runtime.js +843 -0
- package/dist/core/ingestion/language-provider.d.ts +38 -1
- package/dist/core/ingestion/language-provider.js +22 -0
- package/dist/core/ingestion/languages/java/spring-actuator.d.ts +2 -0
- package/dist/core/ingestion/languages/java/spring-actuator.js +62 -0
- package/dist/core/ingestion/languages/java.js +8 -7
- package/dist/core/ingestion/languages/kotlin/spring-actuator.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/spring-actuator.js +172 -0
- package/dist/core/ingestion/languages/kotlin.js +3 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +2 -1
- package/dist/core/ingestion/pipeline-phases/processes.js +39 -4
- package/dist/core/ingestion/pipeline-phases/scan.js +82 -1
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +8 -0
- package/dist/core/ingestion/pipeline.d.ts +8 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
- package/dist/core/ingestion/workers/parse-worker.js +5 -1
- package/dist/core/lbug/csv-generator.js +4 -1
- package/dist/core/lbug/graph-emit-sink.d.ts +3 -2
- package/dist/core/lbug/graph-emit-sink.js +4 -2
- package/dist/core/lbug/lbug-adapter.js +1 -1
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/run-analyze.d.ts +5 -0
- package/dist/core/run-analyze.js +56 -0
- package/dist/mcp/local/local-backend.js +46 -3
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +4 -4
- package/dist/server/analyze-launch.d.ts +1 -0
- package/dist/server/analyze-launch.js +1 -0
- package/dist/server/api.d.ts +4 -0
- package/dist/server/api.js +46 -4
- package/dist/storage/parse-cache.js +8 -1
- package/dist/storage/repo-meta.d.ts +12 -0
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +3 -0
|
@@ -20,7 +20,7 @@ import type { VariableExtractor } from './variable-types.js';
|
|
|
20
20
|
import type { ImportResolverFn } from './import-resolvers/types.js';
|
|
21
21
|
import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
22
22
|
import type { CfgVisitor } from './cfg/types.js';
|
|
23
|
-
import type { NodeLabel } from '../../_shared/index.js';
|
|
23
|
+
import type { GraphNode, NodeLabel } from '../../_shared/index.js';
|
|
24
24
|
import type { ExtractedRoute } from './route-extractors/laravel.js';
|
|
25
25
|
import type { SharedSpringType } from './route-extractors/spring-shared.js';
|
|
26
26
|
import type { ModuleConstants, Operand, RepoConstants } from './route-extractors/constant-resolver.js';
|
|
@@ -31,11 +31,27 @@ export type CaptureMap = Record<string, SyntaxNode | undefined>;
|
|
|
31
31
|
export interface DefinitionPropertiesContext {
|
|
32
32
|
readonly nodeLabel: NodeLabel;
|
|
33
33
|
readonly nodeName: string;
|
|
34
|
+
readonly filePath: string;
|
|
34
35
|
readonly definitionNode: SyntaxNode;
|
|
35
36
|
readonly parsedImports: readonly ParsedImport[];
|
|
36
37
|
readonly isExported: boolean;
|
|
37
38
|
}
|
|
38
39
|
export type DefinitionPropertiesExtractor = (context: DefinitionPropertiesContext) => Readonly<Record<string, unknown>> | undefined;
|
|
40
|
+
export interface RuntimeCallableIdentity {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly descriptorParameterTypes: readonly string[] | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Optional language-owned bridge from runtime/compiler symbol identities to
|
|
46
|
+
* source graph symbols. Framework importers use this instead of naming
|
|
47
|
+
* languages or reproducing compiler conventions in shared ingestion code.
|
|
48
|
+
*/
|
|
49
|
+
export interface RuntimeSymbolStrategy {
|
|
50
|
+
/** Runtime owner names that may contain this callable/property. */
|
|
51
|
+
readonly callableOwnerAliases?: (node: GraphNode, owner: GraphNode | undefined) => readonly string[];
|
|
52
|
+
/** Whether a runtime callable identity can conservatively identify a node. */
|
|
53
|
+
readonly matchesCallable: (node: GraphNode, runtime: RuntimeCallableIdentity) => boolean;
|
|
54
|
+
}
|
|
39
55
|
/** Run optional provider enrichment without allowing one hook failure to drop
|
|
40
56
|
* the rest of the worker's language batch. */
|
|
41
57
|
export declare function runDefinitionPropertiesExtractor(extractor: DefinitionPropertiesExtractor, context: DefinitionPropertiesContext, onError: (error: unknown) => void): Readonly<Record<string, unknown>> | undefined;
|
|
@@ -125,6 +141,12 @@ interface LanguageProviderConfig {
|
|
|
125
141
|
* Default: undefined (no preprocessing — `file.content` is parsed verbatim).
|
|
126
142
|
*/
|
|
127
143
|
readonly preprocessSource?: (sourceText: string, filePath: string) => string;
|
|
144
|
+
/**
|
|
145
|
+
* Runtime/compiler identity reconciliation for framework metadata. The
|
|
146
|
+
* central importer owns ambiguity handling; providers only supply aliases
|
|
147
|
+
* and language-specific callable compatibility.
|
|
148
|
+
*/
|
|
149
|
+
readonly runtimeSymbolStrategy?: RuntimeSymbolStrategy;
|
|
128
150
|
/** Type extraction: declarations, initializers, for-loop bindings */
|
|
129
151
|
readonly typeConfig: LanguageTypeConfig;
|
|
130
152
|
/** Export detection: is this AST node a public/exported symbol? */
|
|
@@ -372,6 +394,15 @@ interface LanguageProviderConfig {
|
|
|
372
394
|
* {@link extractModuleConstants} accepts.
|
|
373
395
|
*/
|
|
374
396
|
readonly moduleConstantHeuristic?: (content: string) => boolean;
|
|
397
|
+
/**
|
|
398
|
+
* Prepare this language's harvested constants once the complete repo map is
|
|
399
|
+
* available and before route operands are folded. The parse phase passes only
|
|
400
|
+
* entries owned by this provider, so implementations can build one reusable
|
|
401
|
+
* language-specific index and may materialize deferred bindings in place.
|
|
402
|
+
*
|
|
403
|
+
* Default: undefined (the harvested constants are already fold-ready).
|
|
404
|
+
*/
|
|
405
|
+
readonly prepareRouteConstants?: (repo: RepoConstants) => void;
|
|
375
406
|
/**
|
|
376
407
|
* Fold one file's non-literal route-path operand list
|
|
377
408
|
* (`routePathExpr`/`routePathOperands` of an `ExtractedDecoratorRoute`)
|
|
@@ -723,6 +754,12 @@ export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrat
|
|
|
723
754
|
/** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
|
|
724
755
|
readonly isBuiltInName: (name: string) => boolean;
|
|
725
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
759
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
760
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
761
|
+
*/
|
|
762
|
+
export declare function prepareRouteConstantsByProvider(repo: RepoConstants, providerForFile: (filePath: string) => Pick<LanguageProvider, 'prepareRouteConstants'> | null): void;
|
|
726
763
|
/** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
|
|
727
764
|
export declare function defineLanguage(config: LanguageProviderConfig): LanguageProvider;
|
|
728
765
|
export {};
|
|
@@ -45,6 +45,28 @@ export function shouldHarvestModuleConstants(provider, content) {
|
|
|
45
45
|
return false;
|
|
46
46
|
return !provider.moduleConstantHeuristic || provider.moduleConstantHeuristic(content);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
50
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
51
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
52
|
+
*/
|
|
53
|
+
export function prepareRouteConstantsByProvider(repo, providerForFile) {
|
|
54
|
+
const slices = new Map();
|
|
55
|
+
for (const [filePath, constants] of repo) {
|
|
56
|
+
const provider = providerForFile(filePath);
|
|
57
|
+
if (!provider?.prepareRouteConstants)
|
|
58
|
+
continue;
|
|
59
|
+
let slice = slices.get(provider);
|
|
60
|
+
if (!slice) {
|
|
61
|
+
slice = new Map();
|
|
62
|
+
slices.set(provider, slice);
|
|
63
|
+
}
|
|
64
|
+
slice.set(filePath, constants);
|
|
65
|
+
}
|
|
66
|
+
for (const [provider, slice] of slices) {
|
|
67
|
+
provider.prepareRouteConstants?.(slice);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
const DEFAULTS = {
|
|
49
71
|
mroStrategy: 'first-wins',
|
|
50
72
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const JVM_PRIMITIVES = {
|
|
2
|
+
B: 'byte',
|
|
3
|
+
C: 'char',
|
|
4
|
+
D: 'double',
|
|
5
|
+
F: 'float',
|
|
6
|
+
I: 'int',
|
|
7
|
+
J: 'long',
|
|
8
|
+
S: 'short',
|
|
9
|
+
Z: 'boolean',
|
|
10
|
+
};
|
|
11
|
+
function normalizedType(value, runtime) {
|
|
12
|
+
let erased = value.trim();
|
|
13
|
+
let arrayDimensions = 0;
|
|
14
|
+
if (erased.endsWith('...')) {
|
|
15
|
+
arrayDimensions++;
|
|
16
|
+
erased = erased.slice(0, -3);
|
|
17
|
+
}
|
|
18
|
+
while (erased.endsWith('[]')) {
|
|
19
|
+
arrayDimensions++;
|
|
20
|
+
erased = erased.slice(0, -2);
|
|
21
|
+
}
|
|
22
|
+
erased = erased.replace(/<.*>$/, '').replaceAll('$', '.').replaceAll('/', '.');
|
|
23
|
+
const simple = erased.slice(erased.lastIndexOf('.') + 1);
|
|
24
|
+
const base = runtime ? (JVM_PRIMITIVES[simple] ?? simple) : simple;
|
|
25
|
+
return `${base}${'[]'.repeat(arrayDimensions)}`;
|
|
26
|
+
}
|
|
27
|
+
function sourceTypeIsUnknown(value) {
|
|
28
|
+
const type = normalizedType(value, false).replace(/(?:\[\])+$/, '');
|
|
29
|
+
return type === '?' || /^[A-Z]$/.test(type);
|
|
30
|
+
}
|
|
31
|
+
function matchesJavaCallable(node, runtime) {
|
|
32
|
+
if (node.label !== 'Method' || node.properties.name !== runtime.name)
|
|
33
|
+
return false;
|
|
34
|
+
const descriptorTypes = runtime.descriptorParameterTypes;
|
|
35
|
+
if (descriptorTypes === undefined)
|
|
36
|
+
return true;
|
|
37
|
+
const parameterCount = node.properties.parameterCount;
|
|
38
|
+
if (typeof parameterCount === 'number' && parameterCount !== descriptorTypes.length)
|
|
39
|
+
return false;
|
|
40
|
+
const sourceTypes = node.properties.parameterTypes;
|
|
41
|
+
if (!Array.isArray(sourceTypes) ||
|
|
42
|
+
sourceTypes.length !== descriptorTypes.length ||
|
|
43
|
+
!sourceTypes.every((type) => typeof type === 'string')) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return sourceTypes.every((sourceType, index) => {
|
|
47
|
+
if (sourceTypeIsUnknown(sourceType))
|
|
48
|
+
return true;
|
|
49
|
+
const source = normalizedType(sourceType, false);
|
|
50
|
+
const descriptor = normalizedType(descriptorTypes[index] ?? '', true);
|
|
51
|
+
if (source === descriptor)
|
|
52
|
+
return true;
|
|
53
|
+
// Java parser metadata currently drops the ellipsis from varargs and also
|
|
54
|
+
// leaves parameterCount open-ended. Only in that shape may T match JVM T[].
|
|
55
|
+
return (typeof parameterCount !== 'number' &&
|
|
56
|
+
descriptor.endsWith('[]') &&
|
|
57
|
+
source === descriptor.slice(0, -2));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export const javaRuntimeSymbolStrategy = {
|
|
61
|
+
matchesCallable: matchesJavaCallable,
|
|
62
|
+
};
|
|
@@ -13,7 +13,7 @@ import { defineLanguage } from '../language-provider.js';
|
|
|
13
13
|
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
15
15
|
import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
|
|
16
|
-
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, } from '../route-extractors/java-const-resolver.js';
|
|
16
|
+
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, prepareJavaRouteConstants, } from '../route-extractors/java-const-resolver.js';
|
|
17
17
|
import { javaExportChecker } from '../export-detection.js';
|
|
18
18
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
19
19
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -30,6 +30,7 @@ import { collectJavaCaptureSideChannel } from './java/capture-side-channel.js';
|
|
|
30
30
|
import { javaRecordMethodExtractor, shouldSkipJavaRecordComponentDefinition, } from './java/record-components.js';
|
|
31
31
|
import { synthesizeLombokAccessors } from './java/lombok-synthesizer.js';
|
|
32
32
|
import { emitJavaScopeCaptures, interpretJavaImport, interpretJavaTypeBinding, javaBindingScopeFor, javaImportOwningScope, javaMergeBindings, javaReceiverBinding, javaArityCompatibility, resolveJavaImportTarget, } from './java/index.js';
|
|
33
|
+
import { javaRuntimeSymbolStrategy } from './java/spring-actuator.js';
|
|
33
34
|
/**
|
|
34
35
|
* Java names the platform owns, matched against a BARE IDENTIFIER — a dropped
|
|
35
36
|
* receiver's chain base (`System` in `System.out.println(...)`) or the bare
|
|
@@ -168,6 +169,7 @@ export const javaProvider = defineLanguage({
|
|
|
168
169
|
shouldSkipDefinitionCapture: shouldSkipJavaRecordComponentDefinition,
|
|
169
170
|
variableExtractor: createVariableExtractor(javaVariableConfig),
|
|
170
171
|
classExtractor: createClassExtractor(javaClassConfig),
|
|
172
|
+
runtimeSymbolStrategy: javaRuntimeSymbolStrategy,
|
|
171
173
|
// ── Javadoc → description (issue #2270) ──
|
|
172
174
|
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
173
175
|
builtInNames: BUILT_INS,
|
|
@@ -202,11 +204,10 @@ export const javaProvider = defineLanguage({
|
|
|
202
204
|
// INTERFACES on this side only, which cost the graph its Route nodes while
|
|
203
205
|
// the group still published the contract).
|
|
204
206
|
moduleConstantHeuristic: (content) => isJavaConstantFile(content) ||
|
|
205
|
-
//
|
|
206
|
-
// constant ref
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/\bimport\s+(?:static\s+)?[\w.]+\s*;/.test(content),
|
|
207
|
+
// Class imports and static (including on-demand) imports can bind a
|
|
208
|
+
// constant ref. Ordinary `import a.b.*;` is not a Java type import and is
|
|
209
|
+
// not expanded by extractJavaModuleConstants, so it must not harvest.
|
|
210
|
+
/\bimport\s+(?:static\s+[\w.]+(?:\.\*)?|[\w.]+)\s*;/.test(content),
|
|
211
|
+
prepareRouteConstants: prepareJavaRouteConstants,
|
|
211
212
|
foldRoutePathOperands: foldJavaOperands,
|
|
212
213
|
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DefinitionPropertiesContext, RuntimeSymbolStrategy } from '../../language-provider.js';
|
|
2
|
+
/** Transient graph metadata used only by the same analysis run's runtime import. */
|
|
3
|
+
export declare function extractKotlinRuntimeSymbolProperties(context: DefinitionPropertiesContext): Readonly<Record<string, unknown>> | undefined;
|
|
4
|
+
export declare const kotlinRuntimeSymbolStrategy: RuntimeSymbolStrategy;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const RUNTIME_OWNER_ALIASES = 'runtimeOwnerAliases';
|
|
3
|
+
const RUNTIME_CALLABLE_ALIASES = 'runtimeCallableAliases';
|
|
4
|
+
const KOTLIN_SUSPEND = 'kotlinSuspend';
|
|
5
|
+
const fileFacadeMetadataCache = new WeakMap();
|
|
6
|
+
function rootNode(node) {
|
|
7
|
+
let current = node;
|
|
8
|
+
while (current.parent)
|
|
9
|
+
current = current.parent;
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
function packageName(root) {
|
|
13
|
+
const header = root.namedChildren.find((child) => child.type === 'package_header');
|
|
14
|
+
return header?.text.replace(/^package\s+/, '').trim() ?? '';
|
|
15
|
+
}
|
|
16
|
+
function qualify(packageNameValue, simpleName) {
|
|
17
|
+
return packageNameValue.length === 0 ? simpleName : `${packageNameValue}.${simpleName}`;
|
|
18
|
+
}
|
|
19
|
+
function standardFacadeName(filePath) {
|
|
20
|
+
const stem = path.basename(filePath).replace(/\.(?:kt|kts)$/i, '');
|
|
21
|
+
return `${stem.charAt(0).toUpperCase()}${stem.slice(1)}Kt`;
|
|
22
|
+
}
|
|
23
|
+
function jvmNameIdentifiers(imports, allowUnqualified) {
|
|
24
|
+
const names = new Set(['kotlin.jvm.JvmName']);
|
|
25
|
+
if (allowUnqualified)
|
|
26
|
+
names.add('JvmName');
|
|
27
|
+
for (const parsedImport of imports) {
|
|
28
|
+
if (parsedImport.kind !== 'named' && parsedImport.kind !== 'alias')
|
|
29
|
+
continue;
|
|
30
|
+
if (parsedImport.importedName !== 'JvmName')
|
|
31
|
+
continue;
|
|
32
|
+
const target = parsedImport.targetRaw.replace(/\\/g, '/');
|
|
33
|
+
if (target === 'kotlin.jvm' || target === 'kotlin.jvm.JvmName') {
|
|
34
|
+
names.add(parsedImport.localName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...names];
|
|
38
|
+
}
|
|
39
|
+
function annotationJvmName(source, target = '', imports = [], allowUnqualified = true) {
|
|
40
|
+
const names = jvmNameIdentifiers(imports, allowUnqualified);
|
|
41
|
+
if (names.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
const escapedTarget = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
const prefix = target.length === 0 ? '' : `${escapedTarget}:`;
|
|
45
|
+
const namePattern = [...names]
|
|
46
|
+
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
47
|
+
.join('|');
|
|
48
|
+
return new RegExp(`@${prefix}(?:${namePattern})\\s*\\(\\s*["']([^"']+)["']\\s*\\)`).exec(source)?.[1];
|
|
49
|
+
}
|
|
50
|
+
function fileFacadeMetadata(root, imports, allowUnqualified) {
|
|
51
|
+
const cached = fileFacadeMetadataCache.get(root);
|
|
52
|
+
if (cached !== undefined)
|
|
53
|
+
return cached;
|
|
54
|
+
const metadata = {
|
|
55
|
+
packageName: packageName(root),
|
|
56
|
+
customFacade: annotationJvmName(root.text, 'file', imports, allowUnqualified),
|
|
57
|
+
};
|
|
58
|
+
fileFacadeMetadataCache.set(root, metadata);
|
|
59
|
+
return metadata;
|
|
60
|
+
}
|
|
61
|
+
function hasEnclosingType(node) {
|
|
62
|
+
let current = node.parent;
|
|
63
|
+
while (current) {
|
|
64
|
+
if (current.type === 'class_declaration' ||
|
|
65
|
+
current.type === 'object_declaration' ||
|
|
66
|
+
current.type === 'companion_object') {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
current = current.parent;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
/** Transient graph metadata used only by the same analysis run's runtime import. */
|
|
74
|
+
export function extractKotlinRuntimeSymbolProperties(context) {
|
|
75
|
+
const properties = {};
|
|
76
|
+
const source = context.definitionNode.text;
|
|
77
|
+
const root = rootNode(context.definitionNode);
|
|
78
|
+
const allowUnqualifiedJvmName = !/\bannotation\s+class\s+JvmName\b/.test(root.text);
|
|
79
|
+
if ((context.nodeLabel === 'Function' || context.nodeLabel === 'Method') &&
|
|
80
|
+
context.definitionNode.type === 'function_declaration') {
|
|
81
|
+
if (/\bsuspend\b/.test(source.slice(0, source.indexOf('fun') + 3))) {
|
|
82
|
+
properties[KOTLIN_SUSPEND] = true;
|
|
83
|
+
}
|
|
84
|
+
const callableJvmName = annotationJvmName(source, '', context.parsedImports, allowUnqualifiedJvmName);
|
|
85
|
+
if (callableJvmName !== undefined) {
|
|
86
|
+
properties[RUNTIME_CALLABLE_ALIASES] = [callableJvmName];
|
|
87
|
+
}
|
|
88
|
+
if (!hasEnclosingType(context.definitionNode)) {
|
|
89
|
+
const facade = fileFacadeMetadata(root, context.parsedImports, allowUnqualifiedJvmName);
|
|
90
|
+
properties[RUNTIME_OWNER_ALIASES] = [
|
|
91
|
+
qualify(facade.packageName, facade.customFacade ?? standardFacadeName(context.filePath)),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (context.nodeLabel === 'Property') {
|
|
96
|
+
const getterJvmName = annotationJvmName(source, 'get', context.parsedImports, allowUnqualifiedJvmName);
|
|
97
|
+
if (getterJvmName !== undefined) {
|
|
98
|
+
properties[RUNTIME_CALLABLE_ALIASES] = [getterJvmName];
|
|
99
|
+
}
|
|
100
|
+
if (!hasEnclosingType(context.definitionNode)) {
|
|
101
|
+
const facade = fileFacadeMetadata(root, context.parsedImports, allowUnqualifiedJvmName);
|
|
102
|
+
properties[RUNTIME_OWNER_ALIASES] = [
|
|
103
|
+
qualify(facade.packageName, facade.customFacade ?? standardFacadeName(context.filePath)),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Object.keys(properties).length === 0 ? undefined : properties;
|
|
108
|
+
}
|
|
109
|
+
function stringArrayProperty(node, property) {
|
|
110
|
+
const value = node.properties[property];
|
|
111
|
+
return Array.isArray(value)
|
|
112
|
+
? value.filter((item) => typeof item === 'string')
|
|
113
|
+
: [];
|
|
114
|
+
}
|
|
115
|
+
function callableNames(node) {
|
|
116
|
+
return [String(node.properties.name), ...stringArrayProperty(node, RUNTIME_CALLABLE_ALIASES)];
|
|
117
|
+
}
|
|
118
|
+
function propertyGetterNames(node) {
|
|
119
|
+
const name = String(node.properties.name);
|
|
120
|
+
const capitalized = `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
121
|
+
return [
|
|
122
|
+
name.startsWith('is') && name.length > 2 && /[A-Z]/.test(name.charAt(2))
|
|
123
|
+
? name
|
|
124
|
+
: `get${capitalized}`,
|
|
125
|
+
...stringArrayProperty(node, RUNTIME_CALLABLE_ALIASES),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function sourceCallableName(runtimeName) {
|
|
129
|
+
return runtimeName.endsWith('$default') ? runtimeName.slice(0, -'$default'.length) : runtimeName;
|
|
130
|
+
}
|
|
131
|
+
function matchesKotlinCallable(node, runtime) {
|
|
132
|
+
// Kotlin property declarations and their synthesized JVM accessor Methods
|
|
133
|
+
// coexist in the graph. Bind runtime getters to the source Property so the
|
|
134
|
+
// synthetic accessor cannot turn an otherwise exact match into ambiguity.
|
|
135
|
+
if (node.properties.synthetic === 'kotlin-jvm')
|
|
136
|
+
return false;
|
|
137
|
+
const runtimeName = sourceCallableName(runtime.name);
|
|
138
|
+
if (node.label === 'Property') {
|
|
139
|
+
const names = propertyGetterNames(node);
|
|
140
|
+
if (!names.includes(runtime.name) && !names.includes(runtimeName))
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
else if (!callableNames(node).includes(runtimeName)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const parameterCount = node.properties.parameterCount;
|
|
147
|
+
const descriptorTypes = runtime.descriptorParameterTypes;
|
|
148
|
+
if (typeof parameterCount !== 'number' ||
|
|
149
|
+
descriptorTypes === undefined ||
|
|
150
|
+
runtime.name.endsWith('$default')) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (parameterCount === descriptorTypes.length)
|
|
154
|
+
return true;
|
|
155
|
+
return (node.properties[KOTLIN_SUSPEND] === true &&
|
|
156
|
+
parameterCount + 1 === descriptorTypes.length &&
|
|
157
|
+
descriptorTypes.at(-1) === 'kotlin/coroutines/Continuation');
|
|
158
|
+
}
|
|
159
|
+
export const kotlinRuntimeSymbolStrategy = {
|
|
160
|
+
callableOwnerAliases(node, owner) {
|
|
161
|
+
const aliases = [...stringArrayProperty(node, RUNTIME_OWNER_ALIASES)];
|
|
162
|
+
const ownerName = owner?.properties.qualifiedName;
|
|
163
|
+
if (typeof ownerName === 'string') {
|
|
164
|
+
aliases.push(ownerName);
|
|
165
|
+
if (node.properties.isStatic === true && !ownerName.endsWith('.Companion')) {
|
|
166
|
+
aliases.push(`${ownerName}.Companion`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return aliases;
|
|
170
|
+
},
|
|
171
|
+
matchesCallable: matchesKotlinCallable,
|
|
172
|
+
};
|
|
@@ -29,6 +29,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|
|
29
29
|
import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js';
|
|
30
30
|
import { collectKotlinCaptureSideChannel, emitKotlinScopeCaptures, interpretKotlinImport, interpretKotlinTypeBinding, kotlinArityCompatibility, kotlinBindingScopeFor, kotlinImportOwningScope, kotlinMergeBindings, kotlinReceiverBinding, } from './kotlin/index.js';
|
|
31
31
|
import { synthesizeLombokAccessors } from './kotlin/lombok-synthesizer.js';
|
|
32
|
+
import { extractKotlinRuntimeSymbolProperties, kotlinRuntimeSymbolStrategy, } from './kotlin/spring-actuator.js';
|
|
32
33
|
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
|
|
33
34
|
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
|
|
34
35
|
* Returns true when the captured definition node has a class_body ancestor. */
|
|
@@ -157,6 +158,8 @@ export const kotlinProvider = defineLanguage({
|
|
|
157
158
|
builtInNames: BUILT_INS,
|
|
158
159
|
// ── KDoc → description (issue #2270) ──
|
|
159
160
|
descriptionExtractor: createLeadingDocDescriptionExtractor(),
|
|
161
|
+
definitionPropertiesExtractor: extractKotlinRuntimeSymbolProperties,
|
|
162
|
+
runtimeSymbolStrategy: kotlinRuntimeSymbolStrategy,
|
|
160
163
|
labelOverride: (functionNode, defaultLabel) => {
|
|
161
164
|
if (defaultLabel !== 'Function')
|
|
162
165
|
return defaultLabel;
|
|
@@ -30,7 +30,8 @@ import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
|
|
|
30
30
|
import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
|
|
31
31
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
32
32
|
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
33
|
-
import { resolveOperands
|
|
33
|
+
import { resolveOperands } from '../route-extractors/python-const-resolver.js';
|
|
34
|
+
import { prepareRouteConstantsByProvider } from '../language-provider.js';
|
|
34
35
|
import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
|
|
35
36
|
import fs from 'node:fs';
|
|
36
37
|
import { effectiveRamBytes, memoryAutopilotDisabled } from '../utils/effective-ram.js';
|
|
@@ -1022,6 +1023,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1022
1023
|
for (const { filePath, constants } of allModuleConstants) {
|
|
1023
1024
|
repoConstants.set(filePath, constants);
|
|
1024
1025
|
}
|
|
1026
|
+
// Let each language prepare only its own constants slice before folding.
|
|
1027
|
+
// This is where deferred wildcard bindings can be materialized once per
|
|
1028
|
+
// provider without naming a language in the shared parse phase.
|
|
1029
|
+
prepareRouteConstantsByProvider(repoConstants, getProviderForFile);
|
|
1025
1030
|
const resolvedRoutes = [];
|
|
1026
1031
|
let skipped = 0;
|
|
1027
1032
|
for (const dr of allDecoratorRoutes) {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Detects execution flows (processes) and creates Process nodes +
|
|
5
5
|
* STEP_IN_PROCESS edges. Also links Route/Tool nodes to processes.
|
|
6
6
|
*
|
|
7
|
-
* @deps communities, routes, tools,
|
|
7
|
+
* @deps communities, routes, tools, springAutoConfiguration,
|
|
8
|
+
* pruneLocalSymbols, structure, parse
|
|
8
9
|
* @reads graph (all nodes and relationships), communityResult, routeRegistry,
|
|
9
10
|
* toolDefs, parse's allFetchCalls + allORMQueries (R3-6 sink sites)
|
|
10
11
|
* @writes graph (Process nodes, STEP_IN_PROCESS edges, ENTRY_POINT_OF edges)
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Detects execution flows (processes) and creates Process nodes +
|
|
5
5
|
* STEP_IN_PROCESS edges. Also links Route/Tool nodes to processes.
|
|
6
6
|
*
|
|
7
|
-
* @deps communities, routes, tools,
|
|
7
|
+
* @deps communities, routes, tools, springAutoConfiguration,
|
|
8
|
+
* pruneLocalSymbols, structure, parse
|
|
8
9
|
* @reads graph (all nodes and relationships), communityResult, routeRegistry,
|
|
9
10
|
* toolDefs, parse's allFetchCalls + allORMQueries (R3-6 sink sites)
|
|
10
11
|
* @writes graph (Process nodes, STEP_IN_PROCESS edges, ENTRY_POINT_OF edges)
|
|
@@ -39,7 +40,15 @@ export const processesPhase = {
|
|
|
39
40
|
// sinks rather than failing the phase. `pruneLocalSymbols` is declared
|
|
40
41
|
// explicitly so process extraction always reads the trimmed graph even if a
|
|
41
42
|
// future option drops the intervening `mro`/`communities` phases.
|
|
42
|
-
deps: [
|
|
43
|
+
deps: [
|
|
44
|
+
'communities',
|
|
45
|
+
'routes',
|
|
46
|
+
'tools',
|
|
47
|
+
'springAutoConfiguration',
|
|
48
|
+
'pruneLocalSymbols',
|
|
49
|
+
'structure',
|
|
50
|
+
'parse',
|
|
51
|
+
],
|
|
43
52
|
async execute(ctx, deps) {
|
|
44
53
|
const { totalFiles } = getPhaseOutput(deps, 'structure');
|
|
45
54
|
const { communityResult } = getPhaseOutput(deps, 'communities');
|
|
@@ -172,8 +181,34 @@ export const processesPhase = {
|
|
|
172
181
|
step: step.step,
|
|
173
182
|
});
|
|
174
183
|
});
|
|
184
|
+
// The static registry is finalized before Spring runtime enrichment. Merge
|
|
185
|
+
// runtime-confirmed Route nodes from the graph after the explicit
|
|
186
|
+
// springAutoConfiguration dependency has completed, so Actuator-only
|
|
187
|
+
// mappings participate in the same process-linking path.
|
|
188
|
+
const processRouteRegistry = new Map(routeRegistry);
|
|
189
|
+
ctx.graph.forEachNode((node) => {
|
|
190
|
+
if (node.label !== 'Route' ||
|
|
191
|
+
node.properties.runtimeSource !== 'spring-actuator' ||
|
|
192
|
+
node.properties.runtimeConfirmed !== true) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const url = typeof node.properties.name === 'string' ? node.properties.name : undefined;
|
|
196
|
+
const filePath = typeof node.properties.filePath === 'string' ? node.properties.filePath : undefined;
|
|
197
|
+
const method = typeof node.properties.method === 'string' ? node.properties.method : undefined;
|
|
198
|
+
if (url === undefined || filePath === undefined)
|
|
199
|
+
return;
|
|
200
|
+
const key = routeNodeKey(method, url);
|
|
201
|
+
if (!processRouteRegistry.has(key)) {
|
|
202
|
+
processRouteRegistry.set(key, {
|
|
203
|
+
filePath,
|
|
204
|
+
source: 'spring-actuator-runtime',
|
|
205
|
+
url,
|
|
206
|
+
...(method === undefined ? {} : { method }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
175
210
|
// Link Route and Tool nodes to Processes
|
|
176
|
-
if (
|
|
211
|
+
if (processRouteRegistry.size > 0 || toolDefs.length > 0) {
|
|
177
212
|
// Two-tier route lookup, mirroring the tool tables 10 lines below.
|
|
178
213
|
// Routes whose handler resolved key by `handlerSymbolId` (read from
|
|
179
214
|
// the Route node's graph properties — routes.ts stamps it there) and
|
|
@@ -188,7 +223,7 @@ export const processesPhase = {
|
|
|
188
223
|
// routes phase stamps on the Route node was never consulted.
|
|
189
224
|
const routesByHandlerId = new Map();
|
|
190
225
|
const routesWithoutHandlerByFile = new Map();
|
|
191
|
-
for (const [, entry] of
|
|
226
|
+
for (const [, entry] of processRouteRegistry) {
|
|
192
227
|
// Push the Route node identity (`routeNodeKey`), not the bare URL, so the
|
|
193
228
|
// ENTRY_POINT_OF edge targets the same node id the routes phase created
|
|
194
229
|
// (#2289: a same-URL GET/POST pair is two distinct Route nodes).
|
|
@@ -10,6 +10,79 @@
|
|
|
10
10
|
* @output ScannedFile[], allPaths[], totalFiles
|
|
11
11
|
*/
|
|
12
12
|
import { walkRepositoryPaths } from '../filesystem-walker.js';
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
const SPRING_ACTUATOR_ENDPOINT_FILES = new Set([
|
|
16
|
+
'mappings.json',
|
|
17
|
+
'beans.json',
|
|
18
|
+
'conditions.json',
|
|
19
|
+
'configprops.json',
|
|
20
|
+
'env.json',
|
|
21
|
+
]);
|
|
22
|
+
async function canonicalPath(filePath) {
|
|
23
|
+
return fs.realpath(filePath).catch(() => path.resolve(filePath));
|
|
24
|
+
}
|
|
25
|
+
async function compileActuatorExclusions(repoPath, inputPaths) {
|
|
26
|
+
const repo = await canonicalPath(repoPath);
|
|
27
|
+
const lexicalRepo = path.resolve(repoPath);
|
|
28
|
+
const compiled = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const inputPath of inputPaths) {
|
|
31
|
+
const lexicalInput = path.resolve(repoPath, inputPath);
|
|
32
|
+
const lexicalRelative = path.relative(lexicalRepo, lexicalInput);
|
|
33
|
+
const canonicalInput = await canonicalPath(lexicalInput);
|
|
34
|
+
const candidateInputs = new Set([canonicalInput]);
|
|
35
|
+
if (lexicalRelative === '' ||
|
|
36
|
+
(lexicalRelative !== '..' &&
|
|
37
|
+
!lexicalRelative.startsWith(`..${path.sep}`) &&
|
|
38
|
+
!path.isAbsolute(lexicalRelative))) {
|
|
39
|
+
// Preserve the configured in-repo alias as an exclusion even when its
|
|
40
|
+
// real target is outside the repository.
|
|
41
|
+
candidateInputs.add(path.resolve(repo, lexicalRelative));
|
|
42
|
+
}
|
|
43
|
+
for (const input of candidateInputs) {
|
|
44
|
+
const inputRelativeToRepo = path.relative(repo, input);
|
|
45
|
+
if (inputRelativeToRepo === '') {
|
|
46
|
+
if (seen.has(''))
|
|
47
|
+
continue;
|
|
48
|
+
seen.add('');
|
|
49
|
+
compiled.push({ kind: 'repo-root-endpoints' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (inputRelativeToRepo === '..' ||
|
|
53
|
+
inputRelativeToRepo.startsWith(`..${path.sep}`) ||
|
|
54
|
+
path.isAbsolute(inputRelativeToRepo)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (seen.has(input))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(input);
|
|
60
|
+
compiled.push({ kind: 'dir', resolved: input });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { repo, exclusions: compiled };
|
|
64
|
+
}
|
|
65
|
+
function matchesActuatorExclusion(canonicalRepoPath, filePath, exclusions) {
|
|
66
|
+
if (exclusions.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
const candidate = path.resolve(canonicalRepoPath, filePath);
|
|
69
|
+
for (const exclusion of exclusions) {
|
|
70
|
+
if (exclusion.kind === 'repo-root-endpoints') {
|
|
71
|
+
const candidateRelativeToRepo = path.relative(canonicalRepoPath, candidate);
|
|
72
|
+
if (path.dirname(candidateRelativeToRepo) === '.' &&
|
|
73
|
+
SPRING_ACTUATOR_ENDPOINT_FILES.has(path.basename(candidateRelativeToRepo).toLowerCase())) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const relative = path.relative(exclusion.resolved, candidate);
|
|
79
|
+
if (relative === '' ||
|
|
80
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
13
86
|
export const scanPhase = {
|
|
14
87
|
name: 'scan',
|
|
15
88
|
deps: [],
|
|
@@ -19,15 +92,20 @@ export const scanPhase = {
|
|
|
19
92
|
percent: 0,
|
|
20
93
|
message: 'Scanning repository...',
|
|
21
94
|
});
|
|
95
|
+
const { repo: canonicalRepoPath, exclusions: actuatorExclusions } = await compileActuatorExclusions(ctx.repoPath, [
|
|
96
|
+
...(ctx.options?.springActuatorPath === undefined ? [] : [ctx.options.springActuatorPath]),
|
|
97
|
+
...(ctx.options?.springActuatorScanExclusions ?? []),
|
|
98
|
+
]);
|
|
22
99
|
let scannedFiles;
|
|
23
100
|
try {
|
|
24
101
|
scannedFiles = await walkRepositoryPaths(ctx.repoPath, (current, total, filePath) => {
|
|
25
102
|
const scanProgress = Math.round((current / total) * 15);
|
|
103
|
+
const isRuntimeInput = matchesActuatorExclusion(canonicalRepoPath, filePath, actuatorExclusions);
|
|
26
104
|
ctx.onProgress({
|
|
27
105
|
phase: 'extracting',
|
|
28
106
|
percent: scanProgress,
|
|
29
107
|
message: 'Scanning repository...',
|
|
30
|
-
detail: filePath,
|
|
108
|
+
...(isRuntimeInput ? {} : { detail: filePath }),
|
|
31
109
|
stats: {
|
|
32
110
|
filesProcessed: current,
|
|
33
111
|
totalFiles: total,
|
|
@@ -45,6 +123,9 @@ export const scanPhase = {
|
|
|
45
123
|
}
|
|
46
124
|
throw err;
|
|
47
125
|
}
|
|
126
|
+
if (actuatorExclusions.length > 0) {
|
|
127
|
+
scannedFiles = scannedFiles.filter((file) => !matchesActuatorExclusion(canonicalRepoPath, file.path, actuatorExclusions));
|
|
128
|
+
}
|
|
48
129
|
const totalFiles = scannedFiles.length;
|
|
49
130
|
const allPaths = scannedFiles.map((f) => f.path);
|
|
50
131
|
ctx.onProgress({
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @reads META-INF/spring.factories and AutoConfiguration.imports
|
|
11
11
|
* @writes Class nodes and DECLARES edges
|
|
12
12
|
*/
|
|
13
|
+
import { type SpringActuatorImportStats } from '../frameworks/spring/actuator-runtime.js';
|
|
13
14
|
import type { PipelinePhase } from './types.js';
|
|
14
15
|
export interface SpringAutoConfigurationEntry {
|
|
15
16
|
readonly className: string;
|
|
@@ -24,6 +25,7 @@ export interface SpringAutoConfigurationOutput {
|
|
|
24
25
|
readonly metadataFiles: number;
|
|
25
26
|
readonly autoConfigurations: number;
|
|
26
27
|
readonly ambiguousAutoConfigurations: number;
|
|
28
|
+
readonly actuatorRuntime?: SpringActuatorImportStats;
|
|
27
29
|
}
|
|
28
30
|
export declare function classifySpringAutoConfigurationMetadata(filePath: string): SpringAutoConfigurationMetadataFile | null;
|
|
29
31
|
/** Parse Boot 2.7+/3.x one-class-per-line auto-configuration imports. */
|