gitnexus 1.6.11-rc.36 → 1.6.11-rc.37
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.
|
@@ -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({
|
|
@@ -261,13 +261,56 @@ export function normalizeHttpPath(p) {
|
|
|
261
261
|
return s === '' ? '/' : s;
|
|
262
262
|
}
|
|
263
263
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
264
|
+
* Strip LEADING template interpolations from a consumer url as gateway/host
|
|
265
|
+
* bindings — the enterprise wrapper shape `` `${serviceClient}/api/v1/x` ``
|
|
266
|
+
* where `${serviceClient}` selects the gateway service, not a route segment.
|
|
267
|
+
* This is consumer-path framework semantics (mirrors how an absolute
|
|
268
|
+
* `https://host/path` url keeps only its path), so it lives here rather than
|
|
269
|
+
* in any one language plugin:
|
|
270
|
+
* - `` `${c}/api/x` `` → `/api/x` (clean prefix; `${c}${d}/api/x` → `/api/x`)
|
|
271
|
+
* - `` `${c}/api/x/${id}` ``→ `/api/x/${id}` (mid/tail interpolations are left for the `{param}` pass)
|
|
272
|
+
* Returns null when the stripped remainder is not a single-slash path:
|
|
273
|
+
* - remainder without `/` — relative fragment (`${c}api/x`) or scheme/host
|
|
274
|
+
* (`${scheme}://${host}/api/x`); whether it is a path depends on
|
|
275
|
+
* unverifiable runtime state, so it is dropped rather than guessed at;
|
|
276
|
+
* - remainder starting with `//` — protocol-relative (`${proto}//host/api/x`);
|
|
277
|
+
* keeping it would later collapse to `/host/api/x`.
|
|
278
|
+
*
|
|
279
|
+
* The `?` in a query string cannot leak into the brace matching: `${...}`
|
|
280
|
+
* spans are matched by braces here (before any `{param}` replacement), and
|
|
281
|
+
* `normalizeHttpPath` splits on `?` only after the whole `${...}` span —
|
|
282
|
+
* including any `?` inside it — has been collapsed to `{param}`. So
|
|
283
|
+
* `` `${c}/api/x?id=${id}` `` reduces to `/api/x` on both orderings.
|
|
268
284
|
*/
|
|
285
|
+
function stripLeadingTemplatePrefix(url) {
|
|
286
|
+
if (!url.startsWith('${'))
|
|
287
|
+
return url;
|
|
288
|
+
const rest = url.replace(/^(?:\$\{[^}]*\})+/, '');
|
|
289
|
+
return rest.startsWith('/') && !rest.startsWith('//') ? rest : null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Placeholder substituted for `${...}` before WHATWG `URL` parsing so the
|
|
293
|
+
* parser cannot percent-encode our own `{param}` markers. A genuine encoded
|
|
294
|
+
* segment like `%7Bfoo%7D` then survives as a literal, instead of being
|
|
295
|
+
* rewritten into braces and folded into `{param}`.
|
|
296
|
+
*
|
|
297
|
+
* Private-use U+E000 cannot appear in a real URL path, so a literal
|
|
298
|
+
* `__gitnexus_http_param__` segment is not rewritten into `{param}`.
|
|
299
|
+
*/
|
|
300
|
+
const CONSUMER_PARAM_SENTINEL = '\uE000';
|
|
301
|
+
const CONSUMER_PARAM_SENTINEL_ENC = '%ee%80%80';
|
|
302
|
+
function restoreConsumerParamSentinel(pathOnly) {
|
|
303
|
+
return pathOnly
|
|
304
|
+
.split(CONSUMER_PARAM_SENTINEL)
|
|
305
|
+
.join('{param}')
|
|
306
|
+
.replace(new RegExp(CONSUMER_PARAM_SENTINEL_ENC, 'gi'), '{param}');
|
|
307
|
+
}
|
|
308
|
+
/** Canonicalize a consumer URL after `stripLeadingTemplatePrefix`. */
|
|
269
309
|
function normalizeConsumerPath(url) {
|
|
270
|
-
const
|
|
310
|
+
const stripped = stripLeadingTemplatePrefix(url.trim());
|
|
311
|
+
if (stripped === null)
|
|
312
|
+
return null;
|
|
313
|
+
const templated = stripped.replace(/\$\{[^}]+\}/g, CONSUMER_PARAM_SENTINEL).trim();
|
|
271
314
|
let pathOnly = templated;
|
|
272
315
|
if (/^https?:\/\//i.test(templated)) {
|
|
273
316
|
try {
|
|
@@ -277,6 +320,7 @@ function normalizeConsumerPath(url) {
|
|
|
277
320
|
pathOnly = templated.replace(/^https?:\/\/[^/]+/i, '');
|
|
278
321
|
}
|
|
279
322
|
}
|
|
323
|
+
pathOnly = restoreConsumerParamSentinel(pathOnly);
|
|
280
324
|
const normalized = normalizeHttpPath(pathOnly || '/');
|
|
281
325
|
const segments = normalized
|
|
282
326
|
.split('/')
|
|
@@ -886,6 +930,13 @@ export class HttpRouteExtractor {
|
|
|
886
930
|
if (d.role !== 'consumer')
|
|
887
931
|
continue;
|
|
888
932
|
const pathNorm = normalizeConsumerPath(d.path);
|
|
933
|
+
// A consumer url that cannot be reduced to a routable path (e.g. a
|
|
934
|
+
// leading template binding that is neither a clean prefix nor a
|
|
935
|
+
// remainder that starts with `/`) is dropped here rather than emitted
|
|
936
|
+
// as a never-matching contract — same treatment the plugins give
|
|
937
|
+
// static relative urls at scan time.
|
|
938
|
+
if (pathNorm === null)
|
|
939
|
+
continue;
|
|
889
940
|
// Resolve the function CONTAINING the fetch/axios call so the consumer
|
|
890
941
|
// contract carries a real symbolUid (was always '' — the gap that left
|
|
891
942
|
// cross-repo trace/impact unable to traverse HTTP links).
|
package/package.json
CHANGED