gitnexus 1.6.11-rc.36 → 1.6.11-rc.38
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 +6 -0
- package/dist/cli/analyze-options.d.ts +7 -0
- package/dist/cli/analyze-watch.js +5 -0
- package/dist/cli/analyze.js +11 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/analysis-feature-registry.d.ts +2 -0
- package/dist/core/analysis-feature-registry.js +15 -0
- package/dist/core/group/extractors/http-patterns/java.js +4 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +84 -31
- package/dist/core/group/extractors/http-patterns/node.js +145 -8
- package/dist/core/group/extractors/http-route-extractor.js +56 -5
- package/dist/core/ingestion/asyncapi/document.d.ts +226 -0
- package/dist/core/ingestion/asyncapi/document.js +758 -0
- package/dist/core/ingestion/asyncapi/protocol.d.ts +79 -0
- package/dist/core/ingestion/asyncapi/protocol.js +203 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +5 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.js +9 -0
- package/dist/core/ingestion/frameworks/spring/destinations.d.ts +25 -0
- package/dist/core/ingestion/frameworks/spring/vendor-prefixes.d.ts +7 -0
- package/dist/core/ingestion/frameworks/spring/vendor-prefixes.js +22 -0
- package/dist/core/ingestion/pipeline-phases/spring-destinations.d.ts +43 -2
- package/dist/core/ingestion/pipeline-phases/spring-destinations.js +202 -3
- package/dist/core/ingestion/pipeline.d.ts +14 -0
- package/dist/core/ingestion/route-extractors/kotlin-spring.js +7 -14
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +30 -1
- package/dist/core/ingestion/route-extractors/spring-shared.js +77 -9
- package/dist/core/ingestion/route-extractors/spring.js +9 -5
- package/dist/core/run-analyze.d.ts +5 -0
- package/dist/core/run-analyze.js +49 -13
- package/dist/mcp/resources.js +7 -0
- package/dist/server/analyze-launch.d.ts +1 -0
- package/dist/server/analyze-launch.js +1 -0
- package/dist/server/api.js +7 -1
- package/dist/storage/repo-meta.d.ts +22 -0
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +1 -0
package/README.md
CHANGED
|
@@ -349,6 +349,12 @@ anchors are deliberately omitted. Add common infrastructure fields such as `/hea
|
|
|
349
349
|
|
|
350
350
|
`--spring-actuator` is explicitly opt-in. The path may be a JSON bundle keyed by `mappings`, `beans`, `conditions`, `configprops`, and/or `env`, or a directory containing endpoint-named JSON files. Runtime mappings and beans confirm matching static nodes; conditions and configuration property keys enrich existing evidence, with conservative runtime-only nodes added when no match exists. The configured input is excluded from source scanning; only normalized repository-relative exclusions are retained for future scans, never absolute paths. Env/configprops values, origins, condition messages, and source names are never persisted or printed. Enabled runs always rebuild because runtime snapshots are external to git freshness; omitting the option later rebuilds once to remove runtime evidence. Project config can set the same path with `springActuator` in `.gitnexusrc`.
|
|
351
351
|
|
|
352
|
+
`--asyncapi-spec` is explicitly opt-in and accepts a directory of AsyncAPI documents or a single document; the path is resolved against the repository root, so a committed `docs/asyncapi` and an absolute cache written by something else both work. Each `operations[]` entry of an **AsyncAPI 3.x** document can contribute a `Destination` node keyed by broker and address, with `action: send` emitting `PUBLISHES_TO` and `action: receive` emitting `CONSUMES_FROM`, so a document and source code that name one address on one broker land on the same node. Edges start at the document, not at a callable — a document states that the service talks to an address, not which method does — and no address a document names is ever attached to an unresolved source site.
|
|
353
|
+
|
|
354
|
+
An operation must name a protocol, either through its own `bindings` or through the `servers[].protocol` of the servers its channel resolves to (a channel that lists no `servers` resolves to all of them); operations that name none are refused, as are operations whose two readings name different brokers, and channels that inherit a multi-protocol server set without choosing. HTTP and WebSocket documents are refused for destination minting: there the host rather than the address names the place, and an HTTP endpoint is already modelled as a `Route`. A parameterized address — a channel declaring `parameters`, or an address containing `{` — is refused rather than keyed: two services publishing `{env}.orders` share a pattern, not a queue. AsyncAPI **2.x is refused** under its own counted reason and never mapped, because its `publish`/`subscribe` are inverted relative to 3.x `send`/`receive` and a naive mapping would reverse the async graph while leaving it connected. Every refusal is counted, and a configured path that yields nothing is reported rather than passed over in silence.
|
|
355
|
+
|
|
356
|
+
Like Actuator snapshots, documents are external to git freshness — replacing one moves no commit and dirties no file — so an enabled run always rebuilds, and the first later run without the option rebuilds once to remove document-derived evidence. There is no glob-based auto-discovery, and the option is unsupported with `--watch`.
|
|
357
|
+
|
|
352
358
|
> **`gitnexus uninstall`** reverses `gitnexus setup` — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified **by bundled gitnexus skill name** (e.g. `gitnexus-cli/`), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass `--force` to apply. Per-repo indexes (`gitnexus clean --all`) and the global npm package (`npm uninstall -g gitnexus`) are left for you to remove.
|
|
353
359
|
|
|
354
360
|
## Remote Embeddings
|
|
@@ -129,6 +129,13 @@ export interface AnalyzeOptions {
|
|
|
129
129
|
* bundle or a directory containing endpoint JSON files. Disabled by default.
|
|
130
130
|
*/
|
|
131
131
|
springActuator?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Explicit local AsyncAPI 3.x document input. Accepts a directory of
|
|
134
|
+
* documents or a single document, resolved against the repository root so an
|
|
135
|
+
* out-of-band cache and a committed directory are equally usable. Disabled by
|
|
136
|
+
* default.
|
|
137
|
+
*/
|
|
138
|
+
asyncapiSpec?: string;
|
|
132
139
|
/** OpenAI-compatible embeddings base URL (incl. /v1). Overrides GITNEXUS_EMBEDDING_URL. */
|
|
133
140
|
embeddingBaseUrl?: string;
|
|
134
141
|
/** Embedding model name. Overrides GITNEXUS_EMBEDDING_MODEL. */
|
|
@@ -76,6 +76,11 @@ export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnored
|
|
|
76
76
|
['--index-only', cli.indexOnly],
|
|
77
77
|
['--skip-git', cli.skipGit],
|
|
78
78
|
['--spring-actuator', cli.springActuator],
|
|
79
|
+
// Rejected under --watch for the same reason as --spring-actuator: the
|
|
80
|
+
// watcher reacts to source changes, and nothing watches an out-of-band
|
|
81
|
+
// document directory. Honouring the flag here would read the documents once
|
|
82
|
+
// and then quietly serve a stale answer for the rest of the session.
|
|
83
|
+
['--asyncapi-spec', cli.asyncapiSpec],
|
|
79
84
|
['walCheckpointThreshold', cli.walCheckpointThreshold],
|
|
80
85
|
['embeddingThreads', cli.embeddingThreads],
|
|
81
86
|
['embeddingBatchSize', cli.embeddingBatchSize],
|
package/dist/cli/analyze.js
CHANGED
|
@@ -786,6 +786,16 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
|
|
|
786
786
|
!setPositiveEnv('--embedding-sub-batch-size', 'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE', options.embeddingSubBatchSize)) {
|
|
787
787
|
return;
|
|
788
788
|
}
|
|
789
|
+
// An empty value resolves to the repository root, so `--asyncapi-spec ""`
|
|
790
|
+
// walks the whole tree — defeating the module's own rule that there is no
|
|
791
|
+
// glob-based auto-discovery, and spending the walk budget on `node_modules`.
|
|
792
|
+
// The HTTP entry point already rejects exactly this value; two doors onto one
|
|
793
|
+
// option must not hold different rules.
|
|
794
|
+
if (options.asyncapiSpec !== undefined && options.asyncapiSpec.trim() === '') {
|
|
795
|
+
cliError(' --asyncapi-spec must be a non-empty path.\n');
|
|
796
|
+
process.exitCode = 1;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
789
799
|
if (options.embeddingDevice) {
|
|
790
800
|
const allowed = new Set(['auto', 'cpu', 'dml', 'cuda', 'wasm']);
|
|
791
801
|
if (!allowed.has(options.embeddingDevice)) {
|
|
@@ -1115,6 +1125,7 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
|
|
|
1115
1125
|
// forwarded to the routes phase consumer scan.
|
|
1116
1126
|
fetchWrappers: options.fetchWrappers,
|
|
1117
1127
|
springActuatorPath: options.springActuator,
|
|
1128
|
+
asyncApiSpecPath: options.asyncapiSpec,
|
|
1118
1129
|
// The CLI always process.exit()s after this returns (success path at the
|
|
1119
1130
|
// end of analyzeCommandImpl, error/interrupt paths via process.exit too),
|
|
1120
1131
|
// so the finalize close skips the native conn/db close — it can double-free
|
package/dist/cli/index.js
CHANGED
|
@@ -85,6 +85,8 @@ program
|
|
|
85
85
|
.option('--workers <n>', 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.')
|
|
86
86
|
.option('--spring-actuator <path>', 'Import local Spring Boot Actuator JSON snapshots (mappings, beans, conditions, ' +
|
|
87
87
|
'configprops, env). Explicit opt-in; disabled by default.')
|
|
88
|
+
.option('--asyncapi-spec <path>', 'Read AsyncAPI 3.x documents from this directory or file and resolve broker ' +
|
|
89
|
+
'addresses from them. Explicit opt-in; disabled by default.')
|
|
88
90
|
.option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
|
|
89
91
|
.option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
|
|
90
92
|
.option('--embedding-sub-batch-size <n>', 'Number of chunks per embedding model call')
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Production registry of independently versioned analysis capabilities. */
|
|
2
|
+
export declare const ANALYSIS_FEATURES: readonly [import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE } from './analysis-features.js';
|
|
2
|
+
import { SPRING_AOP_FEATURE, SPRING_BEAN_INVENTORY_FEATURE, SPRING_CONDITIONALS_FEATURE, SPRING_NON_HTTP_HANDLERS_FEATURE, SPRING_ROUTE_BINDINGS_FEATURE, } from './ingestion/frameworks/spring/analysis-features.js';
|
|
3
|
+
import { JAVA_ENUM_INTERFACE_HERITAGE_FEATURE, JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE, SPRING_CONFIG_BINDINGS_FEATURE, } from './ingestion/languages/java/analysis-features.js';
|
|
4
|
+
/** Production registry of independently versioned analysis capabilities. */
|
|
5
|
+
export const ANALYSIS_FEATURES = [
|
|
6
|
+
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
|
|
7
|
+
SPRING_AOP_FEATURE,
|
|
8
|
+
SPRING_BEAN_INVENTORY_FEATURE,
|
|
9
|
+
SPRING_CONDITIONALS_FEATURE,
|
|
10
|
+
SPRING_NON_HTTP_HANDLERS_FEATURE,
|
|
11
|
+
SPRING_ROUTE_BINDINGS_FEATURE,
|
|
12
|
+
SPRING_CONFIG_BINDINGS_FEATURE,
|
|
13
|
+
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
|
|
14
|
+
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
|
|
15
|
+
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Java from 'tree-sitter-java';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
3
|
+
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, isClassLevelMappingAnnotation, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
5
|
import { expandJavaWildcardStaticImports, extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, prepareJavaRouteConstants, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
6
6
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
@@ -394,12 +394,12 @@ function annotationHasRouteMember(annotation) {
|
|
|
394
394
|
return false;
|
|
395
395
|
}
|
|
396
396
|
function typeRequestMethods(typeNode) {
|
|
397
|
-
const mappings = declarationAnnotations(typeNode).filter((annotation) => simpleName(annotation.childForFieldName('name')?.text ?? '')
|
|
397
|
+
const mappings = declarationAnnotations(typeNode).filter((annotation) => isClassLevelMappingAnnotation(simpleName(annotation.childForFieldName('name')?.text ?? '')));
|
|
398
398
|
if (mappings.length === 0)
|
|
399
399
|
return ['*'];
|
|
400
400
|
if (mappings.length !== 1)
|
|
401
401
|
return [];
|
|
402
|
-
return springAnnotationHttpMethods('RequestMapping', mappings[0].text);
|
|
402
|
+
return springAnnotationHttpMethods(simpleName(mappings[0].childForFieldName('name')?.text ?? 'RequestMapping'), mappings[0].text);
|
|
403
403
|
}
|
|
404
404
|
function hasAnnotation(node, names) {
|
|
405
405
|
const modifiers = node.namedChildren.find((child) => child.type === 'modifiers');
|
|
@@ -551,7 +551,7 @@ function scanRouteAnnotations(tree) {
|
|
|
551
551
|
}
|
|
552
552
|
// Type-level (class or interface): a Spring `@RequestMapping` URL prefix, or
|
|
553
553
|
// — on an interface — an OpenFeign `@FeignClient(path = "...")` prefix.
|
|
554
|
-
if (ann
|
|
554
|
+
if (isClassLevelMappingAnnotation(ann)) {
|
|
555
555
|
if (!isRouteMemberKey(keyNode))
|
|
556
556
|
continue;
|
|
557
557
|
if (!valueNode) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.js';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
import {
|
|
3
|
+
import { findEnclosingClass, intersectSpringHttpMethods, isClassLevelMappingAnnotation, joinPath, springAnnotationHttpMethods, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { buildKotlinConstantIndex, extractKotlinModuleConstants, foldKotlinOperands, isKotlinConstantFile, overlayKotlinConstantIndex, parseKotlinConstOperands, unfoldableDeclarationsOf, unquoteKotlinIdentifier, } from '../../../ingestion/route-extractors/kotlin-const-resolver.js';
|
|
5
5
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
6
6
|
/**
|
|
@@ -392,6 +392,13 @@ function inferKotlinOkHttpMethod(urlCall) {
|
|
|
392
392
|
}
|
|
393
393
|
return name === null ? 'GET' : name.toUpperCase();
|
|
394
394
|
}
|
|
395
|
+
function enclosingAnnotationText(node) {
|
|
396
|
+
for (let current = node; current; current = current.parent) {
|
|
397
|
+
if (current.type === 'annotation')
|
|
398
|
+
return current.text;
|
|
399
|
+
}
|
|
400
|
+
return node.text;
|
|
401
|
+
}
|
|
395
402
|
/**
|
|
396
403
|
* Build the plugin only if the Kotlin grammar is available. Compiling
|
|
397
404
|
* the queries against a null grammar would throw at module load time
|
|
@@ -429,7 +436,7 @@ function buildKotlinPlugin(language) {
|
|
|
429
436
|
(modifiers
|
|
430
437
|
(annotation
|
|
431
438
|
(constructor_invocation
|
|
432
|
-
(user_type (type_identifier) @ann (#
|
|
439
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
433
440
|
(value_arguments
|
|
434
441
|
(value_argument . [(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))
|
|
435
442
|
(type_identifier) @cls) @class
|
|
@@ -442,7 +449,7 @@ function buildKotlinPlugin(language) {
|
|
|
442
449
|
(modifiers
|
|
443
450
|
(annotation
|
|
444
451
|
(constructor_invocation
|
|
445
|
-
(user_type (type_identifier) @ann (#
|
|
452
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
446
453
|
(value_arguments
|
|
447
454
|
(value_argument
|
|
448
455
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -457,7 +464,7 @@ function buildKotlinPlugin(language) {
|
|
|
457
464
|
(modifiers
|
|
458
465
|
(annotation
|
|
459
466
|
(constructor_invocation
|
|
460
|
-
(user_type (type_identifier) @ann (#
|
|
467
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
461
468
|
(value_arguments
|
|
462
469
|
(value_argument . ${arrayOfArg('@prefix')})))))
|
|
463
470
|
(type_identifier) @cls) @class
|
|
@@ -470,7 +477,7 @@ function buildKotlinPlugin(language) {
|
|
|
470
477
|
(modifiers
|
|
471
478
|
(annotation
|
|
472
479
|
(constructor_invocation
|
|
473
|
-
(user_type (type_identifier) @ann (#
|
|
480
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
474
481
|
(value_arguments
|
|
475
482
|
(value_argument
|
|
476
483
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -495,7 +502,7 @@ function buildKotlinPlugin(language) {
|
|
|
495
502
|
(modifiers
|
|
496
503
|
(annotation
|
|
497
504
|
(constructor_invocation
|
|
498
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
505
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
499
506
|
(value_arguments
|
|
500
507
|
(value_argument . [(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
501
508
|
(simple_identifier) @method_name) @method
|
|
@@ -508,7 +515,7 @@ function buildKotlinPlugin(language) {
|
|
|
508
515
|
(modifiers
|
|
509
516
|
(annotation
|
|
510
517
|
(constructor_invocation
|
|
511
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
518
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
512
519
|
(value_arguments
|
|
513
520
|
(value_argument
|
|
514
521
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -523,7 +530,7 @@ function buildKotlinPlugin(language) {
|
|
|
523
530
|
(modifiers
|
|
524
531
|
(annotation
|
|
525
532
|
(constructor_invocation
|
|
526
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
533
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
527
534
|
(value_arguments
|
|
528
535
|
(value_argument . ${arrayOfArg('@path')})))))
|
|
529
536
|
(simple_identifier) @method_name) @method
|
|
@@ -536,7 +543,7 @@ function buildKotlinPlugin(language) {
|
|
|
536
543
|
(modifiers
|
|
537
544
|
(annotation
|
|
538
545
|
(constructor_invocation
|
|
539
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
546
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
540
547
|
(value_arguments
|
|
541
548
|
(value_argument
|
|
542
549
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -571,7 +578,7 @@ function buildKotlinPlugin(language) {
|
|
|
571
578
|
(modifiers
|
|
572
579
|
(annotation
|
|
573
580
|
(constructor_invocation
|
|
574
|
-
(user_type (type_identifier) @ann (#
|
|
581
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
575
582
|
(value_arguments (value_argument) @arg))))
|
|
576
583
|
(type_identifier) @cls) @class
|
|
577
584
|
`,
|
|
@@ -589,7 +596,7 @@ function buildKotlinPlugin(language) {
|
|
|
589
596
|
(modifiers
|
|
590
597
|
(annotation
|
|
591
598
|
(constructor_invocation
|
|
592
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
599
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
593
600
|
(value_arguments (value_argument) @arg))))
|
|
594
601
|
(simple_identifier) @method_name) @method
|
|
595
602
|
`,
|
|
@@ -649,8 +656,11 @@ function buildKotlinPlugin(language) {
|
|
|
649
656
|
for (const match of runCompiledPatterns(SPRING_CONST_CLASS_PREFIX_PATTERNS, tree)) {
|
|
650
657
|
const argNode = match.captures.arg;
|
|
651
658
|
const classNode = match.captures.class;
|
|
659
|
+
const annNode = match.captures.ann;
|
|
652
660
|
if (!argNode || !classNode)
|
|
653
661
|
continue;
|
|
662
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
663
|
+
continue;
|
|
654
664
|
if ((resolvedPrefixes.get(classNode.id) ?? []).length > 0)
|
|
655
665
|
continue;
|
|
656
666
|
const expr = kotlinRouteArgumentExpression(argNode);
|
|
@@ -1145,14 +1155,37 @@ function buildKotlinPlugin(language) {
|
|
|
1145
1155
|
return body.namedChildren.filter((c) => c.type === 'function_declaration');
|
|
1146
1156
|
};
|
|
1147
1157
|
const kotlinFunctionName = (fn) => fn.namedChildren.find((c) => c.type === 'simple_identifier')?.text ?? null;
|
|
1158
|
+
const kotlinTypeRequestMethods = (typeNode) => {
|
|
1159
|
+
const modifiers = typeNode.namedChildren.find((child) => child.type === 'modifiers');
|
|
1160
|
+
const mappings = (modifiers?.namedChildren ?? []).filter((annotation) => {
|
|
1161
|
+
if (annotation.type !== 'annotation')
|
|
1162
|
+
return false;
|
|
1163
|
+
return isClassLevelMappingAnnotation(kotlinAnnotationName(annotation) ?? '');
|
|
1164
|
+
});
|
|
1165
|
+
if (mappings.length === 0)
|
|
1166
|
+
return ['*'];
|
|
1167
|
+
if (mappings.length !== 1)
|
|
1168
|
+
return [];
|
|
1169
|
+
const mapping = mappings[0];
|
|
1170
|
+
const mappingName = kotlinAnnotationName(mapping);
|
|
1171
|
+
if (!mappingName)
|
|
1172
|
+
return [];
|
|
1173
|
+
return springAnnotationHttpMethods(mappingName, mapping.text);
|
|
1174
|
+
};
|
|
1175
|
+
const kotlinClassHttpMethodsById = (tree) => new Map(tree.rootNode
|
|
1176
|
+
.descendantsOfType('class_declaration')
|
|
1177
|
+
.map((typeNode) => [typeNode.id, kotlinTypeRequestMethods(typeNode)]));
|
|
1148
1178
|
const collectKotlinSpringTypes = (filePath, tree) => {
|
|
1149
1179
|
// Class-level @RequestMapping prefixes (reuse the provider class-prefix query).
|
|
1150
1180
|
const prefixByClassId = new Map();
|
|
1151
1181
|
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
1152
1182
|
const prefixNode = match.captures.prefix;
|
|
1153
1183
|
const classNode = match.captures.class;
|
|
1184
|
+
const annNode = match.captures.ann;
|
|
1154
1185
|
if (!prefixNode || !classNode)
|
|
1155
1186
|
continue;
|
|
1187
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
1188
|
+
continue;
|
|
1156
1189
|
// An INTERPOLATED literal (`"${ApiPaths.BASE}"`) is not a path — unquoting
|
|
1157
1190
|
// its raw text would carry the source spelling into the shared type view
|
|
1158
1191
|
// as a served prefix. Refusing it here is also what lets the unfoldable
|
|
@@ -1172,6 +1205,7 @@ function buildKotlinPlugin(language) {
|
|
|
1172
1205
|
// noise into the shared type view, so it is left out — the same skip floor
|
|
1173
1206
|
// `java.ts`'s `collectSpringTypes` keeps.
|
|
1174
1207
|
const routesByMethodId = new Map();
|
|
1208
|
+
const classHttpMethodsById = kotlinClassHttpMethodsById(tree);
|
|
1175
1209
|
const unfoldablePrefixClassIds = collectUnfoldablePrefixClassIds(tree, prefixByClassId);
|
|
1176
1210
|
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1177
1211
|
const annNode = match.captures.ann;
|
|
@@ -1179,8 +1213,8 @@ function buildKotlinPlugin(language) {
|
|
|
1179
1213
|
const methodNode = match.captures.method;
|
|
1180
1214
|
if (!annNode || !pathNode || !methodNode)
|
|
1181
1215
|
continue;
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1216
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1217
|
+
if (httpMethods.length === 0)
|
|
1184
1218
|
continue;
|
|
1185
1219
|
const rawPath = unquoteLiteral(pathNode.text);
|
|
1186
1220
|
if (rawPath === null)
|
|
@@ -1190,8 +1224,11 @@ function buildKotlinPlugin(language) {
|
|
|
1190
1224
|
const owner = findEnclosingClass(methodNode);
|
|
1191
1225
|
if (owner && unfoldablePrefixClassIds.has(owner.id))
|
|
1192
1226
|
continue;
|
|
1227
|
+
const constrainedMethods = intersectSpringHttpMethods(owner ? (classHttpMethodsById.get(owner.id) ?? ['*']) : ['*'], httpMethods);
|
|
1193
1228
|
const arr = routesByMethodId.get(methodNode.id) ?? [];
|
|
1194
|
-
|
|
1229
|
+
for (const httpMethod of constrainedMethods) {
|
|
1230
|
+
arr.push({ method: httpMethod, path: rawPath });
|
|
1231
|
+
}
|
|
1195
1232
|
routesByMethodId.set(methodNode.id, arr);
|
|
1196
1233
|
}
|
|
1197
1234
|
const out = [];
|
|
@@ -1319,8 +1356,11 @@ function buildKotlinPlugin(language) {
|
|
|
1319
1356
|
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
1320
1357
|
const prefixNode = match.captures.prefix;
|
|
1321
1358
|
const classNode = match.captures.class;
|
|
1359
|
+
const annNode = match.captures.ann;
|
|
1322
1360
|
if (!prefixNode || !classNode)
|
|
1323
1361
|
continue;
|
|
1362
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
1363
|
+
continue;
|
|
1324
1364
|
// An INTERPOLATED literal (`"${ApiPaths.BASE}"`) is not a path — see
|
|
1325
1365
|
// `isPlainStringLiteral`. Refusing it here also lets the unfoldable
|
|
1326
1366
|
// analysis below mark such a class, since that skips classes whose
|
|
@@ -1366,24 +1406,27 @@ function buildKotlinPlugin(language) {
|
|
|
1366
1406
|
// Literal and constant-valued paths are normalized into one candidate list
|
|
1367
1407
|
// so both reach the same Feign/interface/prefix classification below.
|
|
1368
1408
|
const methodRoutes = [];
|
|
1409
|
+
const classHttpMethodsById = kotlinClassHttpMethodsById(tree);
|
|
1369
1410
|
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1370
1411
|
const annNode = match.captures.ann;
|
|
1371
1412
|
const pathNode = match.captures.path;
|
|
1372
1413
|
const methodNode = match.captures.method;
|
|
1373
1414
|
if (!annNode || !pathNode || !methodNode)
|
|
1374
1415
|
continue;
|
|
1375
|
-
const
|
|
1376
|
-
if (
|
|
1416
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1417
|
+
if (httpMethods.length === 0)
|
|
1377
1418
|
continue;
|
|
1378
1419
|
const rawPath = unquoteLiteral(pathNode.text);
|
|
1379
1420
|
if (rawPath === null)
|
|
1380
1421
|
continue;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1422
|
+
for (const httpMethod of httpMethods) {
|
|
1423
|
+
methodRoutes.push({
|
|
1424
|
+
httpMethod,
|
|
1425
|
+
rawPath,
|
|
1426
|
+
nameNode: match.captures.method_name,
|
|
1427
|
+
methodNode,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1387
1430
|
}
|
|
1388
1431
|
for (const match of runCompiledPatterns(SPRING_CONST_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1389
1432
|
const annNode = match.captures.ann;
|
|
@@ -1391,8 +1434,8 @@ function buildKotlinPlugin(language) {
|
|
|
1391
1434
|
const methodNode = match.captures.method;
|
|
1392
1435
|
if (!annNode || !argNode || !methodNode)
|
|
1393
1436
|
continue;
|
|
1394
|
-
const
|
|
1395
|
-
if (
|
|
1437
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1438
|
+
if (httpMethods.length === 0)
|
|
1396
1439
|
continue;
|
|
1397
1440
|
const expr = kotlinRouteArgumentExpression(argNode);
|
|
1398
1441
|
if (!expr || !FOLDABLE_PATH_EXPRESSIONS.has(expr.type))
|
|
@@ -1413,14 +1456,24 @@ function buildKotlinPlugin(language) {
|
|
|
1413
1456
|
const rawPath = foldKotlinOperands(fileKey, operands, index.repo, kotlinEnclosingTypeNames(methodNode), index);
|
|
1414
1457
|
if (rawPath === null)
|
|
1415
1458
|
continue;
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1459
|
+
for (const httpMethod of httpMethods) {
|
|
1460
|
+
methodRoutes.push({
|
|
1461
|
+
httpMethod,
|
|
1462
|
+
rawPath,
|
|
1463
|
+
nameNode: match.captures.method_name,
|
|
1464
|
+
methodNode,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1422
1467
|
}
|
|
1423
|
-
|
|
1468
|
+
const constrainedMethodRoutes = methodRoutes.flatMap((route) => {
|
|
1469
|
+
const owner = findEnclosingClass(route.methodNode);
|
|
1470
|
+
const classMethods = owner ? (classHttpMethodsById.get(owner.id) ?? ['*']) : ['*'];
|
|
1471
|
+
return intersectSpringHttpMethods(classMethods, [route.httpMethod]).map((httpMethod) => ({
|
|
1472
|
+
...route,
|
|
1473
|
+
httpMethod,
|
|
1474
|
+
}));
|
|
1475
|
+
});
|
|
1476
|
+
for (const { httpMethod, rawPath, nameNode, methodNode } of constrainedMethodRoutes) {
|
|
1424
1477
|
const enclosingClass = findEnclosingClass(methodNode);
|
|
1425
1478
|
// A @(Get|...)Mapping inside a @FeignClient interface is an OpenFeign
|
|
1426
1479
|
// consumer (a remote call), not a route this service serves.
|
|
@@ -3,7 +3,7 @@ import JavaScript from 'tree-sitter-javascript';
|
|
|
3
3
|
import TypeScript from 'tree-sitter-typescript';
|
|
4
4
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
5
5
|
import { MAX_FOLD_LENGTH } from '../../../ingestion/route-extractors/constant-resolver.js';
|
|
6
|
-
import { DATA_ROUTE_TABLE_SOURCE, scanDataRouteTables, } from '../../../ingestion/route-extractors/data-route-table.js';
|
|
6
|
+
import { DATA_ROUTE_TABLE_SOURCE, propertyName, scanDataRouteTables, } from '../../../ingestion/route-extractors/data-route-table.js';
|
|
7
7
|
import { extractNestRoutes } from '../../../ingestion/route-extractors/nest.js';
|
|
8
8
|
import { normalizeExtractedRoutePath } from '../../../ingestion/route-extractors/route-path.js';
|
|
9
9
|
import { buildJsRepoFacts, extractJsModuleFacts, isAxiosNamespace, isHttpClientRef, resolveJsPathExpression, } from '../../../ingestion/route-extractors/js-const-resolver.js';
|
|
@@ -123,6 +123,35 @@ const AXIOS_OBJECT_SPEC = {
|
|
|
123
123
|
arguments: (arguments (object) @options))
|
|
124
124
|
`,
|
|
125
125
|
};
|
|
126
|
+
// ─── Consumer: wrapped client X.request({ url, method }) ────────────
|
|
127
|
+
// Enterprise wrapper shape: an axios instance (or a named request helper)
|
|
128
|
+
// re-exported under a local name — `httpClient.request({ url, method })`
|
|
129
|
+
// from `@winex-plugin/win-request`, `$http.request(...)`. Generic names
|
|
130
|
+
// like `api` need axios.create/import proof (`isHttpClientRef`); spelling
|
|
131
|
+
// alone is too common (graphql-request helpers, domain `api` objects).
|
|
132
|
+
// The member property is `request` (not an HTTP verb), so this cannot
|
|
133
|
+
// collide with the Express provider pattern (`router.get`) or the axios
|
|
134
|
+
// member form (`axios.get`). Option keys are resolved programmatically,
|
|
135
|
+
// same as the jQuery ajax / axios object forms.
|
|
136
|
+
//
|
|
137
|
+
// The query captures the receiver so scan can reject unrelated
|
|
138
|
+
// `.request({ url })` APIs (`cy.request`, `queue.request`).
|
|
139
|
+
const REQUEST_OBJECT_SPEC = {
|
|
140
|
+
meta: {},
|
|
141
|
+
query: `
|
|
142
|
+
(call_expression
|
|
143
|
+
function: (member_expression
|
|
144
|
+
object: (_) @obj
|
|
145
|
+
property: (property_identifier) @fn (#eq? @fn "request"))
|
|
146
|
+
arguments: (arguments . (object) @options))
|
|
147
|
+
`,
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Receivers admitted as wrapped HTTP clients without axios.create proof.
|
|
151
|
+
* Spelling-only: the last identifier in `obj.text` (`this.$http` → `$http`).
|
|
152
|
+
* Keep this set small — every extra name is a false-positive surface.
|
|
153
|
+
*/
|
|
154
|
+
const WRAPPED_REQUEST_RECEIVERS = new Set(['httpClient', '$http']);
|
|
126
155
|
function compileBundle(language, name) {
|
|
127
156
|
const mk = (spec, suffix) => compilePatterns({
|
|
128
157
|
name: `${name}-${suffix}`,
|
|
@@ -137,6 +166,7 @@ function compileBundle(language, name) {
|
|
|
137
166
|
jqueryShorthand: mk(JQUERY_SHORTHAND_SPEC, 'jquery-shorthand'),
|
|
138
167
|
jqueryAjax: mk(JQUERY_AJAX_SPEC, 'jquery-ajax'),
|
|
139
168
|
axiosObject: mk(AXIOS_OBJECT_SPEC, 'axios-object'),
|
|
169
|
+
requestObject: mk(REQUEST_OBJECT_SPEC, 'request-object'),
|
|
140
170
|
};
|
|
141
171
|
}
|
|
142
172
|
const JAVASCRIPT_BUNDLE = compileBundle(JavaScript, 'javascript-http');
|
|
@@ -148,6 +178,7 @@ const TSX_BUNDLE = compileBundle(TypeScript.tsx, 'tsx-http');
|
|
|
148
178
|
* of `keyNames`. Returns null when no matching pair is present or the
|
|
149
179
|
* value is not a string literal. Used by the jQuery ajax / axios object
|
|
150
180
|
* consumers to resolve `url` / `method` / `type` keys in any order.
|
|
181
|
+
* Keys use shared `propertyName` so quoted `"method"` matches `method`.
|
|
151
182
|
*/
|
|
152
183
|
function readStringProp(objectNode, keyNames) {
|
|
153
184
|
for (let i = 0; i < objectNode.namedChildCount; i++) {
|
|
@@ -158,7 +189,8 @@ function readStringProp(objectNode, keyNames) {
|
|
|
158
189
|
const valueNode = pair.childForFieldName('value');
|
|
159
190
|
if (!keyNode || !valueNode)
|
|
160
191
|
continue;
|
|
161
|
-
|
|
192
|
+
const key = propertyName(keyNode);
|
|
193
|
+
if (key === null || !keyNames.includes(key))
|
|
162
194
|
continue;
|
|
163
195
|
if (valueNode.type !== 'string' && valueNode.type !== 'template_string')
|
|
164
196
|
continue;
|
|
@@ -168,6 +200,77 @@ function readStringProp(objectNode, keyNames) {
|
|
|
168
200
|
}
|
|
169
201
|
return null;
|
|
170
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Verb for wrapped `X.request({ url, method|type })`. Absent key → GET
|
|
205
|
+
* (same default as fetch-without-options / jQuery ajax). Present but not a
|
|
206
|
+
* string/template, supplied only via object spread, or later overwritten by
|
|
207
|
+
* a duplicate key / spread → `*` so matching can still link without pinning GET.
|
|
208
|
+
* Later properties win, matching JavaScript object-literal evaluation.
|
|
209
|
+
*/
|
|
210
|
+
function readRequestMethod(objectNode, keyNames = ['method', 'type']) {
|
|
211
|
+
let last = { kind: 'absent' };
|
|
212
|
+
for (let i = 0; i < objectNode.namedChildCount; i++) {
|
|
213
|
+
const child = objectNode.namedChild(i);
|
|
214
|
+
if (!child)
|
|
215
|
+
continue;
|
|
216
|
+
if (child.type === 'spread_element') {
|
|
217
|
+
last = { kind: 'unknown' };
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (child.type === 'shorthand_property_identifier' ||
|
|
221
|
+
child.type === 'shorthand_property_identifier_pattern') {
|
|
222
|
+
if (keyNames.includes(child.text))
|
|
223
|
+
last = { kind: 'unknown' };
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (child.type !== 'pair')
|
|
227
|
+
continue;
|
|
228
|
+
const keyNode = child.childForFieldName('key');
|
|
229
|
+
const valueNode = child.childForFieldName('value');
|
|
230
|
+
if (!keyNode)
|
|
231
|
+
continue;
|
|
232
|
+
const key = propertyName(keyNode);
|
|
233
|
+
if (key === null || !keyNames.includes(key))
|
|
234
|
+
continue;
|
|
235
|
+
if (!valueNode || (valueNode.type !== 'string' && valueNode.type !== 'template_string')) {
|
|
236
|
+
last = { kind: 'unknown' };
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const lit = unquoteLiteral(valueNode.text);
|
|
240
|
+
if (lit === null || lit.includes('${')) {
|
|
241
|
+
last = { kind: 'unknown' };
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
last = { kind: 'literal', value: lit };
|
|
245
|
+
}
|
|
246
|
+
if (last.kind === 'literal')
|
|
247
|
+
return last.value.toUpperCase();
|
|
248
|
+
if (last.kind === 'unknown')
|
|
249
|
+
return '*';
|
|
250
|
+
return 'GET';
|
|
251
|
+
}
|
|
252
|
+
function wrappedRequestReceiverName(receiver) {
|
|
253
|
+
const parts = receiver.split('.');
|
|
254
|
+
return parts[parts.length - 1] ?? receiver;
|
|
255
|
+
}
|
|
256
|
+
/** Axios module / axios.create instance, or a registered wrapper identifier. */
|
|
257
|
+
function isAdmittedWrappedRequestReceiver(receiver, fileKey, facts) {
|
|
258
|
+
if (WRAPPED_REQUEST_RECEIVERS.has(wrappedRequestReceiverName(receiver)))
|
|
259
|
+
return true;
|
|
260
|
+
try {
|
|
261
|
+
const isModule = facts === null || fileKey === undefined
|
|
262
|
+
? receiver === 'axios'
|
|
263
|
+
: isAxiosNamespace(fileKey, receiver, facts);
|
|
264
|
+
if (isModule)
|
|
265
|
+
return true;
|
|
266
|
+
if (!facts || fileKey === undefined)
|
|
267
|
+
return false;
|
|
268
|
+
return isHttpClientRef(fileKey, receiver, facts);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
171
274
|
/**
|
|
172
275
|
* Map each named import's LOCAL binding to its DECLARED export name and source
|
|
173
276
|
* module, by walking the file's `import { x as y } from 'm'` statements. Lets
|
|
@@ -335,9 +438,10 @@ function resolveFactsFor(repoContext, fileRel) {
|
|
|
335
438
|
*
|
|
336
439
|
* `/{param}` matches every one-segment provider route in the group, and
|
|
337
440
|
* `matching.exclude_links_param_only_paths` defaults to `false`. A path whose
|
|
338
|
-
* leading term is an unresolved placeholder is refused
|
|
339
|
-
*
|
|
340
|
-
*
|
|
441
|
+
* leading term is an unresolved placeholder is refused unless the next
|
|
442
|
+
* character is `/` — that is the gateway-prefix shape
|
|
443
|
+
* `` `${serviceClient}/api/v1/x` `` that `stripLeadingTemplatePrefix` keeps.
|
|
444
|
+
* Bare `{param}` and `{param}api/x` stay rejected: nothing pins a route.
|
|
341
445
|
*/
|
|
342
446
|
function looksLikeHttpPath(path) {
|
|
343
447
|
if (path === '')
|
|
@@ -352,7 +456,7 @@ function looksLikeHttpPath(path) {
|
|
|
352
456
|
const shape = path.replace(/\$\{[^}]+\}/g, '{param}');
|
|
353
457
|
if (/\s/.test(shape))
|
|
354
458
|
return false;
|
|
355
|
-
if (shape.startsWith('{param}'))
|
|
459
|
+
if (shape.startsWith('{param}') && !shape.startsWith('{param}/'))
|
|
356
460
|
return false;
|
|
357
461
|
// An all-digit string is a path only when it is written as one. A leading
|
|
358
462
|
// slash is that evidence: `client.get('/123')` is a route whose segment the
|
|
@@ -613,8 +717,7 @@ function scanBundle(bundle, tree, repoContext, fileRel) {
|
|
|
613
717
|
const path = readStringProp(optionsNode, ['url']);
|
|
614
718
|
if (path === null)
|
|
615
719
|
continue;
|
|
616
|
-
const
|
|
617
|
-
const method = (rawMethod ?? 'GET').toUpperCase();
|
|
720
|
+
const method = readRequestMethod(optionsNode, ['method']);
|
|
618
721
|
out.push({
|
|
619
722
|
role: 'consumer',
|
|
620
723
|
framework: 'axios',
|
|
@@ -625,6 +728,40 @@ function scanBundle(bundle, tree, repoContext, fileRel) {
|
|
|
625
728
|
confidence: 0.7,
|
|
626
729
|
});
|
|
627
730
|
}
|
|
731
|
+
// Consumer: wrapped client `X.request({ url, method })` — the shared
|
|
732
|
+
// enterprise axios-instance shape (`httpClient.request` from
|
|
733
|
+
// win-request and friends). Emit the raw url (templates intact) so
|
|
734
|
+
// shared `normalizeConsumerPath` can strip a leading `${…}` gateway
|
|
735
|
+
// prefix and fold mid/tail interpolations to `{param}`. A plugin-side
|
|
736
|
+
// longest-slash-segment reducer would truncate those mid-templates
|
|
737
|
+
// before the shared normalizer ever saw them. This scan drops only
|
|
738
|
+
// static relative urls (no `${`, no leading `/`, not `https?://`).
|
|
739
|
+
// That filter is request-wrapper-specific: fetch/axios member forms
|
|
740
|
+
// already admit absolute urls and leave host stripping to
|
|
741
|
+
// `normalizeConsumerPath`.
|
|
742
|
+
for (const match of runCompiledPatterns(bundle.requestObject, tree)) {
|
|
743
|
+
const optionsNode = match.captures.options;
|
|
744
|
+
const objNode = match.captures.obj;
|
|
745
|
+
if (!optionsNode || !objNode)
|
|
746
|
+
continue;
|
|
747
|
+
if (!isAdmittedWrappedRequestReceiver(objNode.text, fileKey, facts))
|
|
748
|
+
continue;
|
|
749
|
+
const rawUrl = readStringProp(optionsNode, ['url']);
|
|
750
|
+
if (rawUrl === null)
|
|
751
|
+
continue;
|
|
752
|
+
const url = rawUrl.trim();
|
|
753
|
+
if (!url.includes('${') && !url.startsWith('/') && !/^https?:\/\//i.test(url))
|
|
754
|
+
continue;
|
|
755
|
+
out.push({
|
|
756
|
+
role: 'consumer',
|
|
757
|
+
framework: 'request',
|
|
758
|
+
method: readRequestMethod(optionsNode),
|
|
759
|
+
path: url,
|
|
760
|
+
name: null,
|
|
761
|
+
line: optionsNode.startPosition.row + 1,
|
|
762
|
+
confidence: 0.65,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
628
765
|
for (const route of scanDataRouteTables(tree)) {
|
|
629
766
|
const imported = route.handlerLocalName === undefined ? undefined : importMap.get(route.handlerLocalName);
|
|
630
767
|
out.push({
|