ucn 3.8.23 → 3.8.25
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/.claude/skills/ucn/SKILL.md +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -10
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/core/entrypoints.js
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
15
17
|
const { getCachedCalls } = require('./callers');
|
|
18
|
+
const { getLanguageModule } = require('../languages');
|
|
16
19
|
|
|
17
20
|
// ============================================================================
|
|
18
21
|
// FRAMEWORK PATTERNS
|
|
@@ -23,14 +26,36 @@ const JS_LANGS = new Set(['javascript', 'typescript', 'tsx']);
|
|
|
23
26
|
const FRAMEWORK_PATTERNS = [
|
|
24
27
|
// ── HTTP Routes ─────────────────────────────────────────────────────
|
|
25
28
|
|
|
26
|
-
// Express
|
|
29
|
+
// Express — call-pattern: app.get('/path', handler), router.get(...), etc.
|
|
27
30
|
{
|
|
28
31
|
id: 'express-route',
|
|
29
32
|
languages: JS_LANGS,
|
|
30
33
|
type: 'http',
|
|
31
34
|
framework: 'express',
|
|
32
35
|
detection: 'callPattern',
|
|
33
|
-
receiverPattern: /^(app|router|server
|
|
36
|
+
receiverPattern: /^(app|router|server)$/i,
|
|
37
|
+
methodPattern: /^(get|post|put|delete|patch|all|use|options|head)$/,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Fastify — call-pattern: fastify.get('/path', handler)
|
|
41
|
+
{
|
|
42
|
+
id: 'fastify-route',
|
|
43
|
+
languages: JS_LANGS,
|
|
44
|
+
type: 'http',
|
|
45
|
+
framework: 'fastify',
|
|
46
|
+
detection: 'callPattern',
|
|
47
|
+
receiverPattern: /^fastify$/i,
|
|
48
|
+
methodPattern: /^(get|post|put|delete|patch|all|use|options|head|route)$/,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Koa-router — call-pattern: koaRouter.get('/path', handler)
|
|
52
|
+
{
|
|
53
|
+
id: 'koa-route',
|
|
54
|
+
languages: JS_LANGS,
|
|
55
|
+
type: 'http',
|
|
56
|
+
framework: 'koa',
|
|
57
|
+
detection: 'callPattern',
|
|
58
|
+
receiverPattern: /^(koaRouter|koa)$/i,
|
|
34
59
|
methodPattern: /^(get|post|put|delete|patch|all|use|options|head)$/,
|
|
35
60
|
},
|
|
36
61
|
|
|
@@ -125,7 +150,63 @@ const FRAMEWORK_PATTERNS = [
|
|
|
125
150
|
type: 'di',
|
|
126
151
|
framework: 'spring',
|
|
127
152
|
detection: 'modifier',
|
|
128
|
-
|
|
153
|
+
// JAVA-4: Spring DI / IoC core annotations
|
|
154
|
+
pattern: /^(bean|component|service|controller|repository|configuration|restcontroller|restcontrolleradvice|controlleradvice|springbootapplication|springbootconfiguration|enableautoconfiguration|componentscan|conditional(on\w+)?|profile|primary|qualifier|autowired|inject|value|scope|lazy|order|dependson|import|importresource|propertysource)$/,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// JAVA-4: Spring MVC binding/validation annotations on handler-method
|
|
158
|
+
// parameters/methods. Treated as entry points because Spring's
|
|
159
|
+
// DispatcherServlet calls these methods reflectively.
|
|
160
|
+
{
|
|
161
|
+
id: 'spring-mvc-method',
|
|
162
|
+
languages: new Set(['java']),
|
|
163
|
+
type: 'http',
|
|
164
|
+
framework: 'spring-mvc',
|
|
165
|
+
detection: 'modifier',
|
|
166
|
+
pattern: /^(initbinder|modelattribute|exceptionhandler|sessionattributes|requestbody|responsebody|responsestatus|crossorigin|pathvariable|requestparam|requestheader|requestattribute|cookievalue|matrixvariable|validated|valid|validator)$/,
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// JAVA-4: JPA / Hibernate persistence annotations. The persistence
|
|
170
|
+
// provider instantiates and reads these via reflection.
|
|
171
|
+
{
|
|
172
|
+
id: 'jpa-entity',
|
|
173
|
+
languages: new Set(['java']),
|
|
174
|
+
type: 'di',
|
|
175
|
+
framework: 'jpa',
|
|
176
|
+
detection: 'modifier',
|
|
177
|
+
pattern: /^(entity|mappedsuperclass|embeddable|embedded|table|secondarytable|column|id|generatedvalue|sequencegenerator|tablegenerator|version|enumerated|temporal|lob|basic|transient|access|onetomany|manytoone|onetoone|manytomany|joincolumn|joincolumns|jointable|orderby|orderColumn|inheritance|discriminatorcolumn|discriminatorvalue|namedquery|namedqueries|namednativequery|sqlresultsetmapping|fieldresultsetmapping|attributeoverride|attributeoverrides|associationoverride|cacheable|maptkey|mapkeyenumerated|mapkeycolumn|maptemporal|elementcollection|collectiontable|converter|convert|cascade)$/,
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// JAVA-4: JPA / Spring Data query annotation.
|
|
181
|
+
{
|
|
182
|
+
id: 'spring-data-query',
|
|
183
|
+
languages: new Set(['java']),
|
|
184
|
+
type: 'di',
|
|
185
|
+
framework: 'spring-data',
|
|
186
|
+
detection: 'modifier',
|
|
187
|
+
pattern: /^(query|modifying|procedure|namedquery|param|lock|querytype|entitygraph|projection)$/,
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// JAVA-4: Transactional / caching / async / scheduling cross-cutting
|
|
191
|
+
// annotations (Spring AOP / Spring tx).
|
|
192
|
+
{
|
|
193
|
+
id: 'spring-tx',
|
|
194
|
+
languages: new Set(['java']),
|
|
195
|
+
type: 'di',
|
|
196
|
+
framework: 'spring',
|
|
197
|
+
detection: 'modifier',
|
|
198
|
+
pattern: /^(transactional|cacheable|cacheevict|cacheput|caching|enabletransactionmanagement|enablecaching|enableasync|enablescheduling|enableaspectjautoproxy)$/,
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// JAVA-4: JAX-RS / JAX-B / XML binding annotations. Frameworks
|
|
202
|
+
// (Jersey, JAXB, etc.) instantiate and serialize via reflection.
|
|
203
|
+
{
|
|
204
|
+
id: 'jax-binding',
|
|
205
|
+
languages: new Set(['java']),
|
|
206
|
+
type: 'http',
|
|
207
|
+
framework: 'jax-rs',
|
|
208
|
+
detection: 'modifier',
|
|
209
|
+
pattern: /^(path|produces|consumes|provider|webservice|webmethod|webparam|webresult|xmlrootelement|xmlelement|xmlattribute|xmlaccessortype|xmltype|xmltransient|xmlid|xmlidref|xmlschematype|xmlseealso|xmlanyelement|xmlanyattribute)$/,
|
|
129
210
|
},
|
|
130
211
|
|
|
131
212
|
// ── Job Schedulers ──────────────────────────────────────────────────
|
|
@@ -206,6 +287,75 @@ const FRAMEWORK_PATTERNS = [
|
|
|
206
287
|
pattern: /^(Test|Benchmark|Example|Fuzz)[A-Z_]/,
|
|
207
288
|
},
|
|
208
289
|
|
|
290
|
+
// ── Java entry points ─────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
// Java main(String[] args) — JVM entry point
|
|
293
|
+
{
|
|
294
|
+
id: 'java-main',
|
|
295
|
+
languages: new Set(['java']),
|
|
296
|
+
type: 'runtime',
|
|
297
|
+
framework: 'java',
|
|
298
|
+
detection: 'namePattern',
|
|
299
|
+
pattern: /^main$/,
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// JUnit @Test family — Java parser lowercases annotations into `modifiers`,
|
|
303
|
+
// not `decorators`, so detection must run against modifiers.
|
|
304
|
+
// JAVA-4: also include lifecycle (BeforeEach/AfterEach/etc.), nested test
|
|
305
|
+
// classes, extension wiring, and SpringBoot/MVC/Data test slices.
|
|
306
|
+
{
|
|
307
|
+
id: 'java-junit-test',
|
|
308
|
+
languages: new Set(['java']),
|
|
309
|
+
type: 'test',
|
|
310
|
+
framework: 'junit',
|
|
311
|
+
detection: 'modifier',
|
|
312
|
+
pattern: /^(test|parameterizedtest|repeatedtest|testfactory|testtemplate|beforeall|beforeeach|afterall|aftereach|nested|disabled|enabled|enabledon\w*|disabledon\w*|tag|displayname|extendwith|registerextension|testmethodorder|testinstance|timeout|csvsource|valuesource|methodsource|enumsource|argumentssource|csvfilesource)$/,
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
// JAVA-4: Spring Boot test slices and integration test annotations.
|
|
316
|
+
{
|
|
317
|
+
id: 'spring-boot-test',
|
|
318
|
+
languages: new Set(['java']),
|
|
319
|
+
type: 'test',
|
|
320
|
+
framework: 'spring-boot-test',
|
|
321
|
+
detection: 'modifier',
|
|
322
|
+
pattern: /^(springboottest|webmvctest|datajpatest|datamongotest|dataredistest|datacassandratest|jsontest|jdbctest|jooqtest|webfluxtest|restclienttest|graphqltest|autoconfiguremockmvc|autoconfiguredatajpa|autoconfigurewebmvc|mockbean|spybean|mockitobean|spymockitobean|sqlgroup|sql|testpropertysource|activeprofiles|dirtiescontext|recordapplicationevents|contextconfiguration|webappconfiguration|importautoconfiguration|bootstrapwith|testexecutionlisteners|transactionalconfiguration|repeatedtests)$/,
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// Spring HTTP route annotations — same lowercase-modifier rule
|
|
326
|
+
{
|
|
327
|
+
id: 'spring-http-mapping',
|
|
328
|
+
languages: new Set(['java']),
|
|
329
|
+
type: 'http',
|
|
330
|
+
framework: 'spring',
|
|
331
|
+
detection: 'modifier',
|
|
332
|
+
pattern: /^(getmapping|postmapping|putmapping|deletemapping|patchmapping|requestmapping)$/,
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// ── Rust entry points ─────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
// Rust main() — fn main() is the binary entry point
|
|
338
|
+
{
|
|
339
|
+
id: 'rust-main',
|
|
340
|
+
languages: new Set(['rust']),
|
|
341
|
+
type: 'runtime',
|
|
342
|
+
framework: 'rust',
|
|
343
|
+
detection: 'namePattern',
|
|
344
|
+
pattern: /^main$/,
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// Rust #[test] attribute — Rust parser stores attributes as `modifiers`,
|
|
348
|
+
// not `decorators`, so detection has to run against modifiers.
|
|
349
|
+
// (The older tokio-main pattern at line 169 already uses 'modifier' correctly.)
|
|
350
|
+
{
|
|
351
|
+
id: 'rust-test-attr',
|
|
352
|
+
languages: new Set(['rust']),
|
|
353
|
+
type: 'test',
|
|
354
|
+
framework: 'rust',
|
|
355
|
+
detection: 'modifier',
|
|
356
|
+
pattern: /^(test|tokio::test|cfg\(test\))$/,
|
|
357
|
+
},
|
|
358
|
+
|
|
209
359
|
// ── Go Framework Patterns ─────────────────────────────────────────
|
|
210
360
|
|
|
211
361
|
// Cobra CLI framework — RunE, Run, PreRunE etc. assigned to cobra.Command struct fields
|
|
@@ -223,6 +373,72 @@ const FRAMEWORK_PATTERNS = [
|
|
|
223
373
|
// Go goroutine launch — go func() or go handler()
|
|
224
374
|
// (detected separately in namePattern since it's a language feature)
|
|
225
375
|
|
|
376
|
+
// ── JS/TS Runtime Entry Points ────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
// Node CLI / app main: symbols defined in conventionally-named entry files.
|
|
379
|
+
// - bin/* (npm "bin" entries)
|
|
380
|
+
// - index.{js,ts,mjs,cjs} (package main convention)
|
|
381
|
+
// - main.{js,ts,mjs,cjs} (electron main, etc.)
|
|
382
|
+
// - cli.{js,ts,mjs,cjs}
|
|
383
|
+
// - server.{js,ts,mjs,cjs}
|
|
384
|
+
{
|
|
385
|
+
id: 'js-cli-main',
|
|
386
|
+
languages: JS_LANGS,
|
|
387
|
+
type: 'runtime',
|
|
388
|
+
framework: 'node',
|
|
389
|
+
detection: 'filePath',
|
|
390
|
+
// Match files under bin/ (any depth), or top-level index/main/cli/server in any directory.
|
|
391
|
+
// The matcher is run against the project-relative path with forward slashes.
|
|
392
|
+
pathPattern: /(^|\/)bin\/[^/]+\.(js|ts|mjs|cjs)$|(^|\/)(index|main|cli|server)\.(js|ts|mjs|cjs)$/,
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// Node shebang entry: any file whose first bytes are `#!/usr/bin/env node`
|
|
396
|
+
// or `#!/path/to/node`. These are runnable scripts, not libraries.
|
|
397
|
+
{
|
|
398
|
+
id: 'js-shebang-main',
|
|
399
|
+
languages: JS_LANGS,
|
|
400
|
+
type: 'runtime',
|
|
401
|
+
framework: 'node',
|
|
402
|
+
detection: 'shebang',
|
|
403
|
+
shebangPattern: /^#![^\n]*\bnode\b/,
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// Jest/Mocha/Vitest test files: any function defined in a *.test.* /
|
|
407
|
+
// *.spec.* file or under __tests__/, test/, tests/.
|
|
408
|
+
{
|
|
409
|
+
id: 'js-test-file',
|
|
410
|
+
languages: JS_LANGS,
|
|
411
|
+
type: 'test',
|
|
412
|
+
framework: 'jest',
|
|
413
|
+
detection: 'filePath',
|
|
414
|
+
pathPattern: /(^|\/)(__tests__|tests?)\/|\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/,
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// Next.js pages/routes: default-exported functions from files under
|
|
418
|
+
// pages/ or app/ are runtime entry points (rendered/served by Next.js).
|
|
419
|
+
{
|
|
420
|
+
id: 'next-page',
|
|
421
|
+
languages: JS_LANGS,
|
|
422
|
+
type: 'runtime',
|
|
423
|
+
framework: 'next',
|
|
424
|
+
detection: 'filePath',
|
|
425
|
+
pathPattern: /(^|\/)(pages|app)\/.*\.(js|ts|jsx|tsx|mjs|cjs)$/,
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
// ── Python Runtime Entry Points ────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
// __main__.py — package executable entry (python -m pkg).
|
|
431
|
+
// The `if __name__ == '__main__':` guard wraps statements not functions,
|
|
432
|
+
// so we treat any function in __main__.py as runtime-reachable.
|
|
433
|
+
{
|
|
434
|
+
id: 'python-main-module',
|
|
435
|
+
languages: new Set(['python']),
|
|
436
|
+
type: 'runtime',
|
|
437
|
+
framework: 'python',
|
|
438
|
+
detection: 'filePath',
|
|
439
|
+
pathPattern: /(^|\/)__main__\.py$/,
|
|
440
|
+
},
|
|
441
|
+
|
|
226
442
|
// ── Catch-all fallbacks ─────────────────────────────────────────────
|
|
227
443
|
|
|
228
444
|
// Python: any decorator with '.' (attribute access) — framework registration heuristic
|
|
@@ -284,6 +500,18 @@ function matchDecoratorOrModifier(symbol, language) {
|
|
|
284
500
|
/**
|
|
285
501
|
* Build a map of symbol names used as callbacks in framework route-registration calls.
|
|
286
502
|
* Scans the calls cache for call-pattern-based framework detection.
|
|
503
|
+
*
|
|
504
|
+
* BUG M2: only treat a call as a route registration when its first argument is a
|
|
505
|
+
* literal string path. Library implementations such as gin's
|
|
506
|
+
* `group.GET(relativePath, handler)` (where `relativePath` is a parameter, not a
|
|
507
|
+
* literal) would otherwise capture local variable names (`relativePath`,
|
|
508
|
+
* `handler`, `urlPattern`) as if they were handler functions. The string-literal
|
|
509
|
+
* check aligns this with bridge.js's `extractServerRoutes` so the route count in
|
|
510
|
+
* `entrypoints` matches the route count in `endpoints`.
|
|
511
|
+
*
|
|
512
|
+
* For HTTP route patterns we additionally apply Express's dual-purpose API check
|
|
513
|
+
* (1-arg `app.get('env')` is a config getter, not a route registration).
|
|
514
|
+
*
|
|
287
515
|
* @param {object} index - ProjectIndex
|
|
288
516
|
* @returns {Map<string, { framework, type, patternId, method, file, line }>}
|
|
289
517
|
*/
|
|
@@ -310,6 +538,13 @@ function buildCallbackEntrypointMap(index) {
|
|
|
310
538
|
for (const pattern of relevantCallPatterns) {
|
|
311
539
|
if (pattern.receiverPattern.test(call.receiver) &&
|
|
312
540
|
pattern.methodPattern.test(call.name)) {
|
|
541
|
+
// BUG M2 (interpolated paths): align with bridge.js's
|
|
542
|
+
// extractServerRoutes — skip routes whose path is interpolated.
|
|
543
|
+
if (pattern.type === 'http' && call.firstStringArg && call.firstStringArgInterp) continue;
|
|
544
|
+
// BUG M5: Express dual-purpose APIs — 1-arg .get('env') is a
|
|
545
|
+
// config getter, not a route registration.
|
|
546
|
+
if (pattern.framework === 'express' &&
|
|
547
|
+
typeof call.argCount === 'number' && call.argCount < 2) continue;
|
|
313
548
|
routeLines.set(call.line, { pattern, call });
|
|
314
549
|
break;
|
|
315
550
|
}
|
|
@@ -323,6 +558,15 @@ function buildCallbackEntrypointMap(index) {
|
|
|
323
558
|
const route = routeLines.get(call.line);
|
|
324
559
|
if (!route) continue;
|
|
325
560
|
|
|
561
|
+
// BUG M2: only treat as a handler if the name resolves to a
|
|
562
|
+
// project-defined symbol. Library code like gin's
|
|
563
|
+
// `group.GET(relativePath, handler)`
|
|
564
|
+
// (routergroup.go:185) has identifiers that are local parameters,
|
|
565
|
+
// not exported handler functions — they must not be marked as
|
|
566
|
+
// entry points. This aligns the HTTP Routes section with
|
|
567
|
+
// bridge.js's extractServerRoutes.
|
|
568
|
+
if (!index.symbols.has(call.name)) continue;
|
|
569
|
+
|
|
326
570
|
if (!result.has(call.name)) {
|
|
327
571
|
result.set(call.name, {
|
|
328
572
|
framework: route.pattern.framework,
|
|
@@ -386,6 +630,56 @@ function detectEntrypoints(index, options = {}) {
|
|
|
386
630
|
|
|
387
631
|
// Collect name-based patterns for efficient matching
|
|
388
632
|
const namePatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'namePattern');
|
|
633
|
+
const filePathPatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'filePath');
|
|
634
|
+
const shebangPatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'shebang');
|
|
635
|
+
|
|
636
|
+
// 0. Pre-compute per-file pattern matches for filePath and shebang detection.
|
|
637
|
+
// These mark every symbol in a file as an entry point.
|
|
638
|
+
const fileMatches = new Map(); // absolutePath -> { pattern, evidence }[]
|
|
639
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
640
|
+
const lang = fileEntry.language;
|
|
641
|
+
const relPath = (fileEntry.relativePath || filePath).split(path.sep).join('/');
|
|
642
|
+
|
|
643
|
+
// filePath patterns match against the project-relative path
|
|
644
|
+
for (const fp of filePathPatterns) {
|
|
645
|
+
if (!fp.languages.has(lang)) continue;
|
|
646
|
+
if (!fp.pathPattern.test(relPath)) continue;
|
|
647
|
+
if (!fileMatches.has(filePath)) fileMatches.set(filePath, []);
|
|
648
|
+
fileMatches.get(filePath).push({
|
|
649
|
+
pattern: fp,
|
|
650
|
+
evidence: `entry-file: ${relPath}`,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// shebang patterns: read the first ~128 bytes safely.
|
|
655
|
+
if (shebangPatterns.length > 0) {
|
|
656
|
+
const relevant = shebangPatterns.filter(p => p.languages.has(lang));
|
|
657
|
+
if (relevant.length > 0) {
|
|
658
|
+
let head = '';
|
|
659
|
+
try {
|
|
660
|
+
const fd = fs.openSync(filePath, 'r');
|
|
661
|
+
try {
|
|
662
|
+
const buf = Buffer.alloc(128);
|
|
663
|
+
const n = fs.readSync(fd, buf, 0, 128, 0);
|
|
664
|
+
head = buf.slice(0, n).toString('utf8');
|
|
665
|
+
} finally {
|
|
666
|
+
fs.closeSync(fd);
|
|
667
|
+
}
|
|
668
|
+
} catch (_e) { /* unreadable — skip */ }
|
|
669
|
+
if (head) {
|
|
670
|
+
for (const sp of relevant) {
|
|
671
|
+
if (sp.shebangPattern.test(head)) {
|
|
672
|
+
if (!fileMatches.has(filePath)) fileMatches.set(filePath, []);
|
|
673
|
+
fileMatches.get(filePath).push({
|
|
674
|
+
pattern: sp,
|
|
675
|
+
evidence: 'shebang #!node',
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
389
683
|
|
|
390
684
|
// 1. Scan all symbols for decorator/modifier/name-based patterns
|
|
391
685
|
for (const [name, symbols] of index.symbols) {
|
|
@@ -439,7 +733,9 @@ function detectEntrypoints(index, options = {}) {
|
|
|
439
733
|
}
|
|
440
734
|
}
|
|
441
735
|
|
|
442
|
-
// 2. Add call-pattern-based entry points (route handlers)
|
|
736
|
+
// 2. Add call-pattern-based entry points (route handlers).
|
|
737
|
+
// Run BEFORE file-level patterns so framework labels (express, gin, etc.) win
|
|
738
|
+
// over generic file-level labels (e.g. server.js / index.js js-cli-main).
|
|
443
739
|
for (const [name, info] of callbackMap) {
|
|
444
740
|
const fileEntry = index.files.get(info.file);
|
|
445
741
|
const relPath = fileEntry?.relativePath || info.file;
|
|
@@ -460,6 +756,139 @@ function detectEntrypoints(index, options = {}) {
|
|
|
460
756
|
});
|
|
461
757
|
}
|
|
462
758
|
|
|
759
|
+
// 3. Add file-level entry points (filePath / shebang).
|
|
760
|
+
//
|
|
761
|
+
// BUG M6: previously this marked EVERY top-level symbol in a matched file
|
|
762
|
+
// as an entry point — way too broad for shebang/CLI files (where only the
|
|
763
|
+
// main entry function is the real runtime entry; helpers are just helpers).
|
|
764
|
+
//
|
|
765
|
+
// Tightened rule for `js-cli-main` and `js-shebang-main` patterns:
|
|
766
|
+
// Only mark as entry points:
|
|
767
|
+
// - Function whose name is `main` (case-sensitive — Node CLI idiom)
|
|
768
|
+
// - The default export of the file (if present)
|
|
769
|
+
// - Any function targeted by a top-level invocation (e.g. file calls
|
|
770
|
+
// `main()` at module scope → main is the entry)
|
|
771
|
+
// - Any function that contains a top-level `if (require.main === module)
|
|
772
|
+
// { ... }` block (Node CLI idiom)
|
|
773
|
+
//
|
|
774
|
+
// For all other file-level patterns (js-test-file, next-page, python-main-module),
|
|
775
|
+
// continue using the broad behavior of marking every symbol as an entry.
|
|
776
|
+
//
|
|
777
|
+
// These run last so any more-specific framework label registered above wins;
|
|
778
|
+
// here we only catch symbols not already seen. Dedup by (file, name) — not
|
|
779
|
+
// (file, line, name) — so a generic file-level entry doesn't add a duplicate
|
|
780
|
+
// entry alongside a specific framework callback registered at a different line.
|
|
781
|
+
const NARROW_FILE_PATTERNS = new Set(['js-cli-main', 'js-shebang-main']);
|
|
782
|
+
|
|
783
|
+
// For each "narrow" matched file, compute the allowed entry symbol names.
|
|
784
|
+
// Falls back to permissive when nothing identifies a specific entry, to avoid
|
|
785
|
+
// hiding the entire file from reachability seeding.
|
|
786
|
+
const narrowAllowedByFile = new Map(); // absoluteFile -> Set<name> | null (null = allow all)
|
|
787
|
+
if (fileMatches.size > 0) {
|
|
788
|
+
for (const [filePath, fmatches] of fileMatches) {
|
|
789
|
+
const isNarrow = fmatches.every(fm => NARROW_FILE_PATTERNS.has(fm.pattern.id));
|
|
790
|
+
if (!isNarrow) continue;
|
|
791
|
+
|
|
792
|
+
const fileEntry = index.files.get(filePath);
|
|
793
|
+
if (!fileEntry) continue;
|
|
794
|
+
|
|
795
|
+
const allowed = new Set();
|
|
796
|
+
|
|
797
|
+
// (1) Function literally named 'main' is conventional for Node CLI idiom.
|
|
798
|
+
if (index.symbols.has('main')) {
|
|
799
|
+
for (const sym of index.symbols.get('main')) {
|
|
800
|
+
if (sym.file === filePath) allowed.add('main');
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// (2) Default export of the file, if any. Across language exporters
|
|
805
|
+
// we look at several shapes:
|
|
806
|
+
// - `module.exports = X` → type 'module.exports', name = X
|
|
807
|
+
// - `export default X` → isDefault / kind === 'default'
|
|
808
|
+
// - Python __all__ single entry → captured per-language
|
|
809
|
+
const exportDetails = fileEntry.exportDetails || [];
|
|
810
|
+
for (const e of exportDetails) {
|
|
811
|
+
if (!e) continue;
|
|
812
|
+
if (e.isDefault === true || e.kind === 'default' || e.name === 'default' ||
|
|
813
|
+
e.type === 'module.exports' || e.type === 'export-default') {
|
|
814
|
+
if (e.localName) allowed.add(e.localName);
|
|
815
|
+
else if (e.name && e.name !== 'default') allowed.add(e.name);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// (3) Top-level invocation targets: any non-method call with no
|
|
820
|
+
// enclosingFunction is module-load-time, so its callee is reachable.
|
|
821
|
+
// We treat the callee as an entry point (the function being kicked off).
|
|
822
|
+
try {
|
|
823
|
+
const calls = getCachedCalls(index, filePath);
|
|
824
|
+
if (calls && calls.length > 0) {
|
|
825
|
+
for (const c of calls) {
|
|
826
|
+
if (c.enclosingFunction != null) continue;
|
|
827
|
+
if (c.isMethod) continue;
|
|
828
|
+
// Resolve callee names (handles aliased imports)
|
|
829
|
+
const names = c.resolvedNames || (c.resolvedName ? [c.resolvedName] : [c.name]);
|
|
830
|
+
for (const n of names) {
|
|
831
|
+
if (index.symbols.has(n)) allowed.add(n);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// (4) Function containing a top-level `if (require.main === module) { ... }`
|
|
836
|
+
// wrapping calls — captured by treating any non-method call whose
|
|
837
|
+
// enclosingFunction body is at top level. The simplest detection:
|
|
838
|
+
// scan calls inside any function whose body contains the require.main
|
|
839
|
+
// guard. We approximate via the same `enclosingFunction` data: if a
|
|
840
|
+
// function contains top-level invocation-style entries, it's typically
|
|
841
|
+
// identified by the (3) check above. Skip explicit (4) detection here —
|
|
842
|
+
// (3) already covers `if (require.main === module) { main(); }`.
|
|
843
|
+
}
|
|
844
|
+
} catch (_e) { /* best-effort */ }
|
|
845
|
+
|
|
846
|
+
// If we identified at least one specific entry, use that set.
|
|
847
|
+
// Otherwise fall back to permissive (null) so a CLI file with neither
|
|
848
|
+
// `main()` nor a clear default-export is still seeded somehow.
|
|
849
|
+
narrowAllowedByFile.set(filePath, allowed.size > 0 ? allowed : null);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (fileMatches.size > 0) {
|
|
854
|
+
const seenByFileName = new Set();
|
|
855
|
+
for (const r of results) {
|
|
856
|
+
seenByFileName.add(`${r.absoluteFile}:${r.name}`);
|
|
857
|
+
}
|
|
858
|
+
for (const [name, symbols] of index.symbols) {
|
|
859
|
+
for (const symbol of symbols) {
|
|
860
|
+
const fmatches = fileMatches.get(symbol.file);
|
|
861
|
+
if (!fmatches || fmatches.length === 0) continue;
|
|
862
|
+
|
|
863
|
+
// Apply narrow filter: for shebang/cli-main files, only allow
|
|
864
|
+
// identified entry symbols.
|
|
865
|
+
if (narrowAllowedByFile.has(symbol.file)) {
|
|
866
|
+
const allowed = narrowAllowedByFile.get(symbol.file);
|
|
867
|
+
if (allowed && !allowed.has(name)) continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const fileNameKey = `${symbol.file}:${name}`;
|
|
871
|
+
if (seenByFileName.has(fileNameKey)) continue;
|
|
872
|
+
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
873
|
+
if (seen.has(key)) continue;
|
|
874
|
+
seen.add(key);
|
|
875
|
+
seenByFileName.add(fileNameKey);
|
|
876
|
+
const first = fmatches[0];
|
|
877
|
+
results.push({
|
|
878
|
+
name,
|
|
879
|
+
file: symbol.relativePath || symbol.file,
|
|
880
|
+
absoluteFile: symbol.file,
|
|
881
|
+
line: symbol.startLine,
|
|
882
|
+
type: first.pattern.type,
|
|
883
|
+
framework: first.pattern.framework,
|
|
884
|
+
patternId: first.pattern.id,
|
|
885
|
+
evidence: [first.evidence],
|
|
886
|
+
confidence: 0.85,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
463
892
|
// Apply filters
|
|
464
893
|
let filtered = results;
|
|
465
894
|
|
|
@@ -494,6 +923,208 @@ function detectEntrypoints(index, options = {}) {
|
|
|
494
923
|
return filtered;
|
|
495
924
|
}
|
|
496
925
|
|
|
926
|
+
// ============================================================================
|
|
927
|
+
// REACHABILITY
|
|
928
|
+
// ============================================================================
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Build a stable key for a symbol-like object based on its file path and start line.
|
|
932
|
+
* Two functions cannot start at the same line in the same file, so this is unique.
|
|
933
|
+
*
|
|
934
|
+
* @param {string} file - Absolute file path
|
|
935
|
+
* @param {number} line - Start line of the symbol
|
|
936
|
+
* @returns {string} Symbol key (e.g. "/abs/path/file.js:42")
|
|
937
|
+
*/
|
|
938
|
+
function symbolKey(file, line) {
|
|
939
|
+
return `${file}:${line}`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Compute the set of symbols transitively reachable from any detected entry point.
|
|
944
|
+
*
|
|
945
|
+
* Performs BFS through the call graph starting from every entry point (framework
|
|
946
|
+
* handlers, main/init, test functions, etc.) and following findCallees recursively.
|
|
947
|
+
*
|
|
948
|
+
* Result is cached on the index instance as `index._reachableSymbols` to avoid
|
|
949
|
+
* recomputation. Subsequent calls return the cached Set.
|
|
950
|
+
*
|
|
951
|
+
* @param {object} index - ProjectIndex instance
|
|
952
|
+
* @returns {Set<string>} Set of symbol keys (file:startLine) reachable from entry points
|
|
953
|
+
*/
|
|
954
|
+
function computeReachability(index) {
|
|
955
|
+
// PERF-1: when _reachableSymbols was loaded from the disk cache, verify
|
|
956
|
+
// the index hasn't drifted (e.g. because the cache was stale and a partial
|
|
957
|
+
// rebuild ran after load). If the fingerprint doesn't match, drop the
|
|
958
|
+
// cached set and recompute.
|
|
959
|
+
if (index._reachableSymbols) {
|
|
960
|
+
if (index._reachableFingerprint) {
|
|
961
|
+
const { _computeReachabilityFingerprint } = require('./cache');
|
|
962
|
+
const currentFingerprint = _computeReachabilityFingerprint(index);
|
|
963
|
+
if (currentFingerprint === index._reachableFingerprint) {
|
|
964
|
+
return index._reachableSymbols;
|
|
965
|
+
}
|
|
966
|
+
// Drift: drop stale set, recompute below.
|
|
967
|
+
index._reachableSymbols = null;
|
|
968
|
+
index._reachableFingerprint = null;
|
|
969
|
+
} else {
|
|
970
|
+
// Computed in-process this run (no fingerprint) — already trustworthy.
|
|
971
|
+
return index._reachableSymbols;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const reachable = new Set();
|
|
976
|
+
const entryPoints = detectEntrypoints(index);
|
|
977
|
+
|
|
978
|
+
// Seed BFS queue from every entry point's matching symbol(s) in the symbol table.
|
|
979
|
+
// detectEntrypoints returns entry-point hits with absoluteFile + line + name; we resolve
|
|
980
|
+
// each to a real symbol object by matching name and (absoluteFile, line).
|
|
981
|
+
const queue = [];
|
|
982
|
+
for (const ep of entryPoints) {
|
|
983
|
+
const symbols = index.symbols.get(ep.name);
|
|
984
|
+
if (!symbols) continue;
|
|
985
|
+
// Match by absoluteFile + line (entry-point line should match symbol startLine).
|
|
986
|
+
// Fall back to file-only match if line shifted (e.g. file edited after detection).
|
|
987
|
+
const match = symbols.find(s =>
|
|
988
|
+
s.file === ep.absoluteFile && s.startLine === ep.line
|
|
989
|
+
) || symbols.find(s => s.file === ep.absoluteFile);
|
|
990
|
+
if (match) {
|
|
991
|
+
const key = symbolKey(match.file, match.startLine);
|
|
992
|
+
if (!reachable.has(key)) {
|
|
993
|
+
reachable.add(key);
|
|
994
|
+
queue.push(match);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// BUG-BE root cause 1: also seed from per-language getEntryPointKind() predicates.
|
|
1000
|
+
// detectEntrypoints() above only knows about FRAMEWORK_PATTERNS; it does not consult
|
|
1001
|
+
// each language module's getEntryPointKind() (which classifies React lifecycle methods,
|
|
1002
|
+
// @Test annotations, Rust #[cfg(test)] modules, Go Test*/main, etc.). Without this
|
|
1003
|
+
// pass those entries are never seeded, so anything reachable only via them is reported
|
|
1004
|
+
// as unreachable. The reachable Set already dedupes against the framework-pattern pass.
|
|
1005
|
+
const langModuleCache = new Map();
|
|
1006
|
+
for (const [, symbols] of index.symbols) {
|
|
1007
|
+
for (const symbol of symbols) {
|
|
1008
|
+
const fileEntry = index.files.get(symbol.file);
|
|
1009
|
+
if (!fileEntry) continue;
|
|
1010
|
+
const lang = fileEntry.language;
|
|
1011
|
+
let langModule;
|
|
1012
|
+
if (langModuleCache.has(lang)) {
|
|
1013
|
+
langModule = langModuleCache.get(lang);
|
|
1014
|
+
} else {
|
|
1015
|
+
try {
|
|
1016
|
+
langModule = getLanguageModule(lang);
|
|
1017
|
+
} catch (_e) {
|
|
1018
|
+
langModule = null;
|
|
1019
|
+
}
|
|
1020
|
+
langModuleCache.set(lang, langModule);
|
|
1021
|
+
}
|
|
1022
|
+
if (!langModule || !langModule.getEntryPointKind) continue;
|
|
1023
|
+
let kind;
|
|
1024
|
+
try {
|
|
1025
|
+
kind = langModule.getEntryPointKind(symbol);
|
|
1026
|
+
} catch (_e) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (kind == null) continue;
|
|
1030
|
+
const key = symbolKey(symbol.file, symbol.startLine);
|
|
1031
|
+
if (!reachable.has(key)) {
|
|
1032
|
+
reachable.add(key);
|
|
1033
|
+
queue.push(symbol);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// BUG-BE root cause 3: top-level executable code in JS/TS files is a reachability
|
|
1039
|
+
// source. A call expression at module scope (no enclosing function) is invoked
|
|
1040
|
+
// when the module is loaded; treat its callee as reachable. Walk getCachedCalls
|
|
1041
|
+
// (already AST-derived) and seed callees of enclosingFunction === null calls
|
|
1042
|
+
// for files in JS/TS-language. Resolution mirrors findCallees: name lookup +
|
|
1043
|
+
// disambiguation by import bindings is not necessary here — we just need to seed
|
|
1044
|
+
// the callee symbol(s); the BFS below propagates further reachability.
|
|
1045
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
1046
|
+
const lang = fileEntry.language;
|
|
1047
|
+
if (lang !== 'javascript' && lang !== 'typescript' && lang !== 'tsx') continue;
|
|
1048
|
+
let calls;
|
|
1049
|
+
try {
|
|
1050
|
+
calls = getCachedCalls(index, filePath);
|
|
1051
|
+
} catch (_e) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (!calls || calls.length === 0) continue;
|
|
1055
|
+
for (const call of calls) {
|
|
1056
|
+
// Top-level call: AST recorded enclosingFunction === null/undefined.
|
|
1057
|
+
if (call.enclosingFunction != null) continue;
|
|
1058
|
+
// Method calls at top-level (e.g. a.b()) are typically library calls;
|
|
1059
|
+
// we keep the simple identifier case where the callee name is a project symbol.
|
|
1060
|
+
if (call.isMethod) continue;
|
|
1061
|
+
// Resolve possible callee names — supports aliased imports.
|
|
1062
|
+
const names = call.resolvedNames || (call.resolvedName ? [call.resolvedName] : [call.name]);
|
|
1063
|
+
for (const cname of names) {
|
|
1064
|
+
const symbols = index.symbols.get(cname);
|
|
1065
|
+
if (!symbols) continue;
|
|
1066
|
+
for (const sym of symbols) {
|
|
1067
|
+
const key = symbolKey(sym.file, sym.startLine);
|
|
1068
|
+
if (!reachable.has(key)) {
|
|
1069
|
+
reachable.add(key);
|
|
1070
|
+
queue.push(sym);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// BFS: walk callees of every reachable symbol.
|
|
1078
|
+
// findCallees returns full symbol objects for every callee with file/startLine.
|
|
1079
|
+
while (queue.length > 0) {
|
|
1080
|
+
const sym = queue.shift();
|
|
1081
|
+
if (!sym.file || sym.startLine == null) continue;
|
|
1082
|
+
|
|
1083
|
+
let callees;
|
|
1084
|
+
try {
|
|
1085
|
+
callees = index.findCallees(sym, { includeMethods: true });
|
|
1086
|
+
} catch (_e) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!callees || callees.length === 0) continue;
|
|
1090
|
+
|
|
1091
|
+
for (const c of callees) {
|
|
1092
|
+
if (!c.file || c.startLine == null) continue;
|
|
1093
|
+
const key = symbolKey(c.file, c.startLine);
|
|
1094
|
+
if (!reachable.has(key)) {
|
|
1095
|
+
reachable.add(key);
|
|
1096
|
+
queue.push(c);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
index._reachableSymbols = reachable;
|
|
1102
|
+
// Clear any stale fingerprint — this set was computed in-process and is
|
|
1103
|
+
// authoritative for the rest of the process lifetime. (saveCache will
|
|
1104
|
+
// re-fingerprint when persisting.)
|
|
1105
|
+
index._reachableFingerprint = null;
|
|
1106
|
+
// MED-1 (Round 5): mark the set dirty so the surface knows to persist it.
|
|
1107
|
+
// Without this flag, a cache-hit run that triggers reachability (about,
|
|
1108
|
+
// context, deadcode, etc.) would compute the BFS in-memory but not save
|
|
1109
|
+
// it, forcing every subsequent cold invocation to repeat the 7-11s tax.
|
|
1110
|
+
// Cleared in saveCache after a successful write.
|
|
1111
|
+
index.reachabilityDirty = true;
|
|
1112
|
+
return reachable;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Check if a symbol (identified by file + startLine) is reachable from any entry point.
|
|
1117
|
+
* Lazily computes the reachable set on first call.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {object} index - ProjectIndex instance
|
|
1120
|
+
* @param {string} symbolKeyStr - Key of form "file:startLine"
|
|
1121
|
+
* @returns {boolean}
|
|
1122
|
+
*/
|
|
1123
|
+
function isReachable(index, symbolKeyStr) {
|
|
1124
|
+
const reachable = computeReachability(index);
|
|
1125
|
+
return reachable.has(symbolKeyStr);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
497
1128
|
/**
|
|
498
1129
|
* Check if a specific symbol is a framework entry point.
|
|
499
1130
|
* Used by deadcode to exclude framework-registered functions.
|
|
@@ -526,4 +1157,7 @@ module.exports = {
|
|
|
526
1157
|
isFrameworkEntrypoint,
|
|
527
1158
|
matchDecoratorOrModifier,
|
|
528
1159
|
buildCallbackEntrypointMap,
|
|
1160
|
+
computeReachability,
|
|
1161
|
+
isReachable,
|
|
1162
|
+
symbolKey,
|
|
529
1163
|
};
|