circle-ir 3.164.0 → 3.166.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/entry-point-detection.d.ts +28 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +619 -13
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/findings.d.ts +9 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +17 -2
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/index.d.ts +1 -0
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +1 -0
- package/dist/analysis/index.js.map +1 -1
- package/dist/analysis/non-executable-lines.d.ts +37 -0
- package/dist/analysis/non-executable-lines.d.ts.map +1 -0
- package/dist/analysis/non-executable-lines.js +135 -0
- package/dist/analysis/non-executable-lines.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +7 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/require-entry-path.d.ts +6 -2
- package/dist/analysis/require-entry-path.d.ts.map +1 -1
- package/dist/analysis/require-entry-path.js +86 -11
- package/dist/analysis/require-entry-path.js.map +1 -1
- package/dist/browser/circle-ir.js +435 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -431,36 +431,64 @@ function methodIsSupertypeLifecycleEntryPoint(method, enclosingType) {
|
|
|
431
431
|
* 8. Fallback → TIER_3_LIBRARY_API.
|
|
432
432
|
*/
|
|
433
433
|
export function classifyEntryPointTier(method, enclosingType, ctx) {
|
|
434
|
-
// Ship 1: Java only. Other languages route via UNKNOWN = pass-through.
|
|
435
|
-
const language = (ctx.language ?? '').toLowerCase();
|
|
436
|
-
if (language !== 'java')
|
|
437
|
-
return 'TIER_UNKNOWN';
|
|
438
434
|
if (!method)
|
|
439
435
|
return 'TIER_UNKNOWN';
|
|
440
|
-
|
|
441
|
-
//
|
|
436
|
+
const language = (ctx.language ?? '').toLowerCase();
|
|
437
|
+
// Language dispatch — Java retains the original in-line logic below;
|
|
438
|
+
// Python / JS-TS / Go / Bash route to dedicated classifiers added
|
|
439
|
+
// 3.166.0 (cognium-dev #237). Anything else routes via UNKNOWN so
|
|
440
|
+
// consumers pass the finding through unchanged.
|
|
441
|
+
switch (language) {
|
|
442
|
+
case 'java':
|
|
443
|
+
return classifyJavaEntryPoint(method, enclosingType);
|
|
444
|
+
case 'python':
|
|
445
|
+
return classifyPythonEntryPoint(method, enclosingType, ctx);
|
|
446
|
+
case 'javascript':
|
|
447
|
+
case 'typescript':
|
|
448
|
+
case 'tsx':
|
|
449
|
+
case 'jsx':
|
|
450
|
+
return classifyJsTsEntryPoint(method, enclosingType, ctx);
|
|
451
|
+
case 'go':
|
|
452
|
+
return classifyGoEntryPoint(method, enclosingType, ctx);
|
|
453
|
+
case 'bash':
|
|
454
|
+
case 'shell':
|
|
455
|
+
return classifyBashEntryPoint(method, enclosingType, ctx);
|
|
456
|
+
default:
|
|
457
|
+
return 'TIER_UNKNOWN';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Java classifier (original ship 1 logic — kept verbatim under the new
|
|
462
|
+
* language dispatch). Order:
|
|
463
|
+
* 1. Library-facade short-circuit (#128 step 2).
|
|
464
|
+
* 2. Method-level annotation → TIER_1.
|
|
465
|
+
* 3. Class-level annotation → TIER_1.
|
|
466
|
+
* 4. Supertype lifecycle method → TIER_1.
|
|
467
|
+
* 5. `main(String[])` → TIER_1.
|
|
468
|
+
* 6. Fallback → TIER_3_LIBRARY_API.
|
|
469
|
+
*/
|
|
470
|
+
function classifyJavaEntryPoint(method, enclosingType) {
|
|
471
|
+
// 1. Library-facade short-circuit.
|
|
442
472
|
if (classShapeIsLibraryFacade(enclosingType)) {
|
|
443
473
|
return 'TIER_3_LIBRARY_API';
|
|
444
474
|
}
|
|
445
|
-
//
|
|
475
|
+
// 2. Method-level annotation
|
|
446
476
|
if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS)) {
|
|
447
477
|
return 'TIER_1_ENTRY_POINT';
|
|
448
478
|
}
|
|
449
|
-
//
|
|
479
|
+
// 3. Class-level annotation (every public method of a controller is TIER_1)
|
|
450
480
|
if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS)) {
|
|
451
481
|
return 'TIER_1_ENTRY_POINT';
|
|
452
482
|
}
|
|
453
|
-
//
|
|
483
|
+
// 4. Supertype lifecycle method
|
|
454
484
|
if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
|
|
455
485
|
return 'TIER_1_ENTRY_POINT';
|
|
456
486
|
}
|
|
457
|
-
//
|
|
487
|
+
// 5. `public static void main(String[])`
|
|
458
488
|
if (looksLikeMainMethod(method)) {
|
|
459
489
|
return 'TIER_1_ENTRY_POINT';
|
|
460
490
|
}
|
|
461
|
-
//
|
|
462
|
-
// (Intentionally no fall-through here; ctx.callGraph is reserved.)
|
|
463
|
-
// 8. Fallback
|
|
491
|
+
// 6. Fallback
|
|
464
492
|
return 'TIER_3_LIBRARY_API';
|
|
465
493
|
}
|
|
466
494
|
/**
|
|
@@ -485,4 +513,582 @@ export function shouldGateInterproceduralParam(sourceType, enclosingMethod, encl
|
|
|
485
513
|
const tier = classifyEntryPointTier(enclosingMethod, enclosingType, ctx);
|
|
486
514
|
return tier === 'TIER_3_LIBRARY_API';
|
|
487
515
|
}
|
|
516
|
+
// ===========================================================================
|
|
517
|
+
// Polyglot classifiers (cognium-dev #237 — 3.166.0)
|
|
518
|
+
// ===========================================================================
|
|
519
|
+
//
|
|
520
|
+
// Java-primary Tier-1 detection has been in production since 3.128.0. The
|
|
521
|
+
// classifier is extended to Python / JS-TS / Go / Bash to close the ~14
|
|
522
|
+
// polyglot FP tail surfaced by the 2026-06 Tier-2 audit. Each classifier
|
|
523
|
+
// is designed to preserve recall on the OWASP BenchmarkPython
|
|
524
|
+
// (TPR ≥81.2% floor) / Express-family / net/http / Bash test corpora
|
|
525
|
+
// while identifying library-facade methods that should NOT be treated as
|
|
526
|
+
// trust boundaries.
|
|
527
|
+
//
|
|
528
|
+
// Common design:
|
|
529
|
+
// 1. Library-facade PATH short-circuit (`/lib/`, `/libapi/`, `/utils/`,
|
|
530
|
+
// `/helpers/`, `/vendor/`, `/node_modules/`, test dirs) → TIER_3
|
|
531
|
+
// before any framework check. Cheap, high-precision.
|
|
532
|
+
// 2. Framework Tier-1 detection using whatever signal is available
|
|
533
|
+
// per-language (decorator strings, `RuntimeRegistration[]`, call
|
|
534
|
+
// site walk, script-body scan).
|
|
535
|
+
// 3. Fallback: TIER_UNKNOWN — safer than TIER_3 for languages where
|
|
536
|
+
// the classifier's negative signal is thin. `require-entry-path.ts`
|
|
537
|
+
// applies an "empty-entry-point-keys → keep" safety guard for the
|
|
538
|
+
// same reason.
|
|
539
|
+
//
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
/**
|
|
542
|
+
* Path substrings that mark a file as a library / helper / vendored
|
|
543
|
+
* dependency / test file — the enclosing method is invoked BY user
|
|
544
|
+
* code, not AT a trust boundary. Shared across Python / JS-TS / Go /
|
|
545
|
+
* Bash. Matched case-insensitively against the forward-slash-normalized
|
|
546
|
+
* file path with sentinel slashes so `/lib/` matches `src/lib/x.py`
|
|
547
|
+
* but not `src/library-not-quite/x.py`.
|
|
548
|
+
*/
|
|
549
|
+
const POLYGLOT_LIBRARY_PATH_FRAGMENTS = [
|
|
550
|
+
'/lib/',
|
|
551
|
+
'/libapi/',
|
|
552
|
+
'/libs/',
|
|
553
|
+
'/utils/',
|
|
554
|
+
'/util/',
|
|
555
|
+
'/helpers/',
|
|
556
|
+
'/helper/',
|
|
557
|
+
'/interop/',
|
|
558
|
+
'/vendor/',
|
|
559
|
+
'/vendored/',
|
|
560
|
+
'/node_modules/',
|
|
561
|
+
'/dist/',
|
|
562
|
+
'/build/',
|
|
563
|
+
'/_internal/',
|
|
564
|
+
'/__tests__/',
|
|
565
|
+
'/tests/',
|
|
566
|
+
'/test/',
|
|
567
|
+
'/testing/',
|
|
568
|
+
'/spec/',
|
|
569
|
+
'/specs/',
|
|
570
|
+
'/fixtures/',
|
|
571
|
+
'/mocks/',
|
|
572
|
+
'/__mocks__/',
|
|
573
|
+
];
|
|
574
|
+
/**
|
|
575
|
+
* Filename suffixes / substrings for test files. Matched against the
|
|
576
|
+
* lowercased basename after the last `/`.
|
|
577
|
+
*/
|
|
578
|
+
const POLYGLOT_TEST_FILE_MARKERS = [
|
|
579
|
+
'.test.',
|
|
580
|
+
'.spec.',
|
|
581
|
+
'_test.',
|
|
582
|
+
'_spec.',
|
|
583
|
+
'.tests.',
|
|
584
|
+
];
|
|
585
|
+
function pathLooksLikeLibraryOrTest(filePath) {
|
|
586
|
+
if (!filePath)
|
|
587
|
+
return false;
|
|
588
|
+
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
589
|
+
const padded = `/${normalized}/`;
|
|
590
|
+
for (const frag of POLYGLOT_LIBRARY_PATH_FRAGMENTS) {
|
|
591
|
+
if (padded.includes(frag))
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
const slash = normalized.lastIndexOf('/');
|
|
595
|
+
const base = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
596
|
+
for (const marker of POLYGLOT_TEST_FILE_MARKERS) {
|
|
597
|
+
if (base.includes(marker))
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Python classifier
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
/**
|
|
606
|
+
* Python decorator names (bare, without the leading `@` and any
|
|
607
|
+
* dotted receiver) that mark a function as a framework entry point.
|
|
608
|
+
* The classifier tolerates receiver prefixes (`app.route`,
|
|
609
|
+
* `router.get`, `blueprint.post`) — see `pythonDecoratorLooksTier1`.
|
|
610
|
+
*
|
|
611
|
+
* Rationale — recall guard: this list covers Flask, FastAPI, Django
|
|
612
|
+
* view decorators, Click/Typer CLI commands, and Celery/RQ task
|
|
613
|
+
* handlers. OWASP BenchmarkPython is Flask-heavy — `route` alone
|
|
614
|
+
* covers the majority.
|
|
615
|
+
*/
|
|
616
|
+
const PYTHON_TIER_1_DECORATOR_NAMES = new Set([
|
|
617
|
+
// Flask
|
|
618
|
+
'route',
|
|
619
|
+
'get',
|
|
620
|
+
'post',
|
|
621
|
+
'put',
|
|
622
|
+
'delete',
|
|
623
|
+
'patch',
|
|
624
|
+
'options',
|
|
625
|
+
'head',
|
|
626
|
+
'before_request',
|
|
627
|
+
'after_request',
|
|
628
|
+
'errorhandler',
|
|
629
|
+
'teardown_request',
|
|
630
|
+
// FastAPI
|
|
631
|
+
'websocket',
|
|
632
|
+
'websocket_route',
|
|
633
|
+
'api_route',
|
|
634
|
+
'middleware',
|
|
635
|
+
// Django
|
|
636
|
+
'login_required',
|
|
637
|
+
'csrf_exempt',
|
|
638
|
+
'csrf_protect',
|
|
639
|
+
'require_http_methods',
|
|
640
|
+
'require_GET',
|
|
641
|
+
'require_POST',
|
|
642
|
+
'require_safe',
|
|
643
|
+
'permission_required',
|
|
644
|
+
'user_passes_test',
|
|
645
|
+
'staff_member_required',
|
|
646
|
+
'api_view',
|
|
647
|
+
'action',
|
|
648
|
+
'detail_route',
|
|
649
|
+
'list_route',
|
|
650
|
+
'renderer_classes',
|
|
651
|
+
'authentication_classes',
|
|
652
|
+
'permission_classes',
|
|
653
|
+
// Click / Typer
|
|
654
|
+
'command',
|
|
655
|
+
'group',
|
|
656
|
+
// Celery / RQ / dramatiq
|
|
657
|
+
'task',
|
|
658
|
+
'shared_task',
|
|
659
|
+
'periodic_task',
|
|
660
|
+
'actor',
|
|
661
|
+
// aiohttp
|
|
662
|
+
'view',
|
|
663
|
+
// pytest fixtures ARE library callback surfaces (test framework calls
|
|
664
|
+
// them), so we mark them TIER_1 too — a taint sink inside a fixture
|
|
665
|
+
// is genuinely reachable when the test runs.
|
|
666
|
+
'fixture',
|
|
667
|
+
]);
|
|
668
|
+
/**
|
|
669
|
+
* Classify a Python function.
|
|
670
|
+
*/
|
|
671
|
+
function classifyPythonEntryPoint(method, enclosingType, ctx) {
|
|
672
|
+
// 1. Library / test / vendored path → TIER_3.
|
|
673
|
+
if (pathLooksLikeLibraryOrTest(ctx.filePath)) {
|
|
674
|
+
return 'TIER_3_LIBRARY_API';
|
|
675
|
+
}
|
|
676
|
+
// 2. Decorator match. Python decorators are stored in
|
|
677
|
+
// `MethodInfo.annotations` by the core extractor (same channel Java
|
|
678
|
+
// uses). Tolerate `@app.route(...)`, `@blueprint.get(...)`, bare
|
|
679
|
+
// `@task`, etc.
|
|
680
|
+
if (pythonDecoratorLooksTier1(method.annotations)) {
|
|
681
|
+
return 'TIER_1_ENTRY_POINT';
|
|
682
|
+
}
|
|
683
|
+
// 3. RuntimeRegistration handler match. Phase-2 Python extractor
|
|
684
|
+
// populates `runtime_registrations` with resolved handler names
|
|
685
|
+
// for decorator-registered functions — reuse.
|
|
686
|
+
if (methodIsRuntimeRegistrationHandler(method, ctx.runtimeRegistrations)) {
|
|
687
|
+
return 'TIER_1_ENTRY_POINT';
|
|
688
|
+
}
|
|
689
|
+
// 4. Module-level `main()` → TIER_1. This is the CLI-entry convention.
|
|
690
|
+
// `enclosingType === undefined` (or a synthesized module-scope type)
|
|
691
|
+
// means the function is at module top-level.
|
|
692
|
+
if (method.name === 'main' && !enclosingType) {
|
|
693
|
+
return 'TIER_1_ENTRY_POINT';
|
|
694
|
+
}
|
|
695
|
+
if (method.name === 'main' && enclosingType && looksLikeModuleType(enclosingType)) {
|
|
696
|
+
return 'TIER_1_ENTRY_POINT';
|
|
697
|
+
}
|
|
698
|
+
// 5. Convention-private helper (`_foo`) → TIER_3 signal. Weak — only
|
|
699
|
+
// fires as TIER_3 when no framework signal was found.
|
|
700
|
+
if (method.name.startsWith('_') && !method.name.startsWith('__')) {
|
|
701
|
+
return 'TIER_3_LIBRARY_API';
|
|
702
|
+
}
|
|
703
|
+
// 6. Fallback — UNKNOWN keeps recall in the tail. `require-entry-path`
|
|
704
|
+
// only drops when the sink method classifies to a strong Tier via
|
|
705
|
+
// the classifier + reverse-BFS combination.
|
|
706
|
+
return 'TIER_UNKNOWN';
|
|
707
|
+
}
|
|
708
|
+
function pythonDecoratorLooksTier1(annotations) {
|
|
709
|
+
if (!annotations || annotations.length === 0)
|
|
710
|
+
return false;
|
|
711
|
+
for (const raw of annotations) {
|
|
712
|
+
// Strip leading `@`, argument list `(...)`, generic `<...>`.
|
|
713
|
+
let name = raw.replace(/^@/, '').replace(/[<(].*$/, '').trim();
|
|
714
|
+
if (!name)
|
|
715
|
+
continue;
|
|
716
|
+
// Take the last dotted segment: `app.route` → `route`,
|
|
717
|
+
// `flask_restful.Api.add_resource` → `add_resource`.
|
|
718
|
+
const dot = name.lastIndexOf('.');
|
|
719
|
+
if (dot >= 0)
|
|
720
|
+
name = name.slice(dot + 1);
|
|
721
|
+
if (PYTHON_TIER_1_DECORATOR_NAMES.has(name))
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
function looksLikeModuleType(t) {
|
|
727
|
+
// Python extractor emits module-level functions under a synthesized
|
|
728
|
+
// container whose name matches the module (or is empty). Treat any
|
|
729
|
+
// container with no `extends` / `implements` / annotations as a
|
|
730
|
+
// module-scope wrapper.
|
|
731
|
+
return (!t.extends &&
|
|
732
|
+
(!t.implements || t.implements.length === 0) &&
|
|
733
|
+
(!t.annotations || t.annotations.length === 0));
|
|
734
|
+
}
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// JS/TS classifier
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
/**
|
|
739
|
+
* NestJS method-level decorator names. Match against the bare name
|
|
740
|
+
* (leading `@` and `(args)` stripped).
|
|
741
|
+
*/
|
|
742
|
+
const JSTS_TIER_1_METHOD_DECORATORS = new Set([
|
|
743
|
+
// NestJS HTTP method decorators
|
|
744
|
+
'Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options', 'All',
|
|
745
|
+
// NestJS WebSocket
|
|
746
|
+
'SubscribeMessage', 'MessageBody', 'ConnectedSocket',
|
|
747
|
+
// NestJS microservices / event
|
|
748
|
+
'EventPattern', 'MessagePattern', 'GrpcMethod', 'GrpcStreamMethod',
|
|
749
|
+
// Angular / other DI-registered handler hooks
|
|
750
|
+
'HostListener',
|
|
751
|
+
]);
|
|
752
|
+
/**
|
|
753
|
+
* NestJS class-level decorator names.
|
|
754
|
+
*/
|
|
755
|
+
const JSTS_TIER_1_CLASS_DECORATORS = new Set([
|
|
756
|
+
'Controller',
|
|
757
|
+
'RestController',
|
|
758
|
+
'Resolver',
|
|
759
|
+
'WebSocketGateway',
|
|
760
|
+
'Gateway',
|
|
761
|
+
]);
|
|
762
|
+
/**
|
|
763
|
+
* Named exports that mark a file as a Lambda / Next.js App Router
|
|
764
|
+
* route module. Matched against the method name when the enclosing
|
|
765
|
+
* TypeInfo looks like a module-scope container.
|
|
766
|
+
*/
|
|
767
|
+
const JSTS_TIER_1_MODULE_EXPORTS = new Set([
|
|
768
|
+
// AWS Lambda / Google Cloud Functions / Netlify / Vercel
|
|
769
|
+
'handler',
|
|
770
|
+
// Next.js App Router — HTTP method exports
|
|
771
|
+
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
|
|
772
|
+
// SvelteKit / Remix
|
|
773
|
+
'load',
|
|
774
|
+
'action',
|
|
775
|
+
// Next.js middleware
|
|
776
|
+
'middleware',
|
|
777
|
+
]);
|
|
778
|
+
function classifyJsTsEntryPoint(method, enclosingType, ctx) {
|
|
779
|
+
// 1. Library / test / vendored path → TIER_3.
|
|
780
|
+
if (pathLooksLikeLibraryOrTest(ctx.filePath)) {
|
|
781
|
+
return 'TIER_3_LIBRARY_API';
|
|
782
|
+
}
|
|
783
|
+
// 2. NestJS method-level decorator.
|
|
784
|
+
if (annotationsInclude(method.annotations, JSTS_TIER_1_METHOD_DECORATORS)) {
|
|
785
|
+
return 'TIER_1_ENTRY_POINT';
|
|
786
|
+
}
|
|
787
|
+
// 3. NestJS class-level decorator (every public method of a
|
|
788
|
+
// controller is TIER_1).
|
|
789
|
+
if (enclosingType && annotationsInclude(enclosingType.annotations, JSTS_TIER_1_CLASS_DECORATORS)) {
|
|
790
|
+
return 'TIER_1_ENTRY_POINT';
|
|
791
|
+
}
|
|
792
|
+
// 4. Phase-1 RuntimeRegistration handler match. Covers
|
|
793
|
+
// Express (`app.get`, `router.post`, `app.use`), Fastify,
|
|
794
|
+
// Koa, EventEmitter (`.on`).
|
|
795
|
+
if (methodIsRuntimeRegistrationHandler(method, ctx.runtimeRegistrations)) {
|
|
796
|
+
return 'TIER_1_ENTRY_POINT';
|
|
797
|
+
}
|
|
798
|
+
// 5. Module-level named-export entry point (Lambda `handler`,
|
|
799
|
+
// Next.js `GET`, SvelteKit `load`, …).
|
|
800
|
+
if (JSTS_TIER_1_MODULE_EXPORTS.has(method.name) && (!enclosingType || looksLikeModuleType(enclosingType))) {
|
|
801
|
+
return 'TIER_1_ENTRY_POINT';
|
|
802
|
+
}
|
|
803
|
+
// 6. Convention: `main()` at module scope → TIER_1.
|
|
804
|
+
if (method.name === 'main' && (!enclosingType || looksLikeModuleType(enclosingType))) {
|
|
805
|
+
return 'TIER_1_ENTRY_POINT';
|
|
806
|
+
}
|
|
807
|
+
// 7. Fallback — UNKNOWN keeps recall.
|
|
808
|
+
return 'TIER_UNKNOWN';
|
|
809
|
+
}
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
// Go classifier
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
/**
|
|
814
|
+
* Method / function names on receivers that register HTTP handlers.
|
|
815
|
+
* Matched against `CallInfo.method_name` when the receiver looks like
|
|
816
|
+
* a Go HTTP framework (see `GO_HTTP_REGISTRAR_RECEIVERS`).
|
|
817
|
+
*/
|
|
818
|
+
const GO_HTTP_REGISTRAR_METHODS = new Set([
|
|
819
|
+
// net/http
|
|
820
|
+
'HandleFunc', 'Handle',
|
|
821
|
+
// gorilla/mux, chi, echo, gin
|
|
822
|
+
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'Any',
|
|
823
|
+
'Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options',
|
|
824
|
+
// gorilla/chi/gin sub-routers
|
|
825
|
+
'Route', 'Mount', 'Method',
|
|
826
|
+
]);
|
|
827
|
+
/**
|
|
828
|
+
* Receiver expressions that indicate an HTTP router / mux / gin
|
|
829
|
+
* engine / echo group / chi router. Simple string match on the
|
|
830
|
+
* `CallInfo.receiver` field.
|
|
831
|
+
*/
|
|
832
|
+
const GO_HTTP_REGISTRAR_RECEIVERS = [
|
|
833
|
+
'http', // net/http.HandleFunc
|
|
834
|
+
'mux', // gorilla/mux
|
|
835
|
+
'router', // chi/gin/gorilla common
|
|
836
|
+
'r', // gin/chi convention
|
|
837
|
+
'e', // echo convention
|
|
838
|
+
'g', // gin group convention
|
|
839
|
+
'app', // fiber convention
|
|
840
|
+
'engine', // gin engine convention
|
|
841
|
+
'srv', 'server',
|
|
842
|
+
];
|
|
843
|
+
/**
|
|
844
|
+
* Classify a Go function.
|
|
845
|
+
*/
|
|
846
|
+
function classifyGoEntryPoint(method, enclosingType, ctx) {
|
|
847
|
+
// 1. Library / test / vendored path → TIER_3. `_test.go` is picked
|
|
848
|
+
// up by the shared test-marker set below (Go test naming convention).
|
|
849
|
+
if (pathLooksLikeLibraryOrTest(ctx.filePath)) {
|
|
850
|
+
return 'TIER_3_LIBRARY_API';
|
|
851
|
+
}
|
|
852
|
+
// Go-specific test naming: `*_test.go`.
|
|
853
|
+
if (ctx.filePath && ctx.filePath.toLowerCase().endsWith('_test.go')) {
|
|
854
|
+
return 'TIER_3_LIBRARY_API';
|
|
855
|
+
}
|
|
856
|
+
// 2. `main` in `main` package → TIER_1. Go's package is per-file
|
|
857
|
+
// stored on `TypeInfo.package` for the synthesized module type.
|
|
858
|
+
if (method.name === 'main' && enclosingType?.package === 'main') {
|
|
859
|
+
return 'TIER_1_ENTRY_POINT';
|
|
860
|
+
}
|
|
861
|
+
// 3. Function signature shape:
|
|
862
|
+
// func(w http.ResponseWriter, r *http.Request)
|
|
863
|
+
// matches the net/http handler contract. Robust across router
|
|
864
|
+
// frameworks that adapt the same signature.
|
|
865
|
+
if (methodHasNetHttpHandlerSignature(method)) {
|
|
866
|
+
return 'TIER_1_ENTRY_POINT';
|
|
867
|
+
}
|
|
868
|
+
// 4. gRPC handler convention:
|
|
869
|
+
// func (s *server) MethodName(ctx context.Context, req *pb.Req) (*pb.Resp, error)
|
|
870
|
+
// Weak signal alone; require receiver look like `*Server` + first
|
|
871
|
+
// param be `context.Context`.
|
|
872
|
+
if (methodLooksLikeGrpcHandler(method, enclosingType)) {
|
|
873
|
+
return 'TIER_1_ENTRY_POINT';
|
|
874
|
+
}
|
|
875
|
+
// 5. `ir.calls` walk — was this function registered via
|
|
876
|
+
// `http.HandleFunc`, `router.GET`, etc. anywhere in the file?
|
|
877
|
+
// We match the registrar call's second argument (handler
|
|
878
|
+
// identifier) against the method name.
|
|
879
|
+
if (methodIsRegisteredByGoHttpFramework(method, ctx.calls)) {
|
|
880
|
+
return 'TIER_1_ENTRY_POINT';
|
|
881
|
+
}
|
|
882
|
+
// 6. Fallback — UNKNOWN keeps recall.
|
|
883
|
+
return 'TIER_UNKNOWN';
|
|
884
|
+
}
|
|
885
|
+
function methodHasNetHttpHandlerSignature(method) {
|
|
886
|
+
const params = method.parameters ?? [];
|
|
887
|
+
if (params.length !== 2)
|
|
888
|
+
return false;
|
|
889
|
+
const p0 = normalizeGoType(params[0]?.type ?? '');
|
|
890
|
+
const p1 = normalizeGoType(params[1]?.type ?? '');
|
|
891
|
+
const p0IsWriter = p0.includes('http.ResponseWriter') || p0.endsWith('ResponseWriter');
|
|
892
|
+
const p1IsRequest = p1.includes('http.Request') || p1.endsWith('*Request') || p1.endsWith('Request');
|
|
893
|
+
return p0IsWriter && p1IsRequest;
|
|
894
|
+
}
|
|
895
|
+
function methodLooksLikeGrpcHandler(method, enclosingType) {
|
|
896
|
+
if (!enclosingType)
|
|
897
|
+
return false;
|
|
898
|
+
const params = method.parameters ?? [];
|
|
899
|
+
if (params.length < 2)
|
|
900
|
+
return false;
|
|
901
|
+
const p0 = normalizeGoType(params[0]?.type ?? '');
|
|
902
|
+
if (!p0.includes('context.Context') && !p0.endsWith('Context'))
|
|
903
|
+
return false;
|
|
904
|
+
const typeName = enclosingType.name ?? '';
|
|
905
|
+
// Common gRPC server struct suffixes.
|
|
906
|
+
if (!/(Server|Service|Handler)$/.test(typeName))
|
|
907
|
+
return false;
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
function normalizeGoType(t) {
|
|
911
|
+
return t.replace(/\s+/g, '').replace(/^\*+/, '');
|
|
912
|
+
}
|
|
913
|
+
function methodIsRegisteredByGoHttpFramework(method, calls) {
|
|
914
|
+
if (!calls || calls.length === 0)
|
|
915
|
+
return false;
|
|
916
|
+
for (const call of calls) {
|
|
917
|
+
if (!GO_HTTP_REGISTRAR_METHODS.has(call.method_name))
|
|
918
|
+
continue;
|
|
919
|
+
const receiver = call.receiver ?? '';
|
|
920
|
+
if (!goRegistrarReceiverMatches(receiver))
|
|
921
|
+
continue;
|
|
922
|
+
const args = call.arguments ?? [];
|
|
923
|
+
// net/http.HandleFunc(pattern, handler) → handler is arg 1.
|
|
924
|
+
// router.GET(pattern, handler) → handler is arg 1.
|
|
925
|
+
// Variadic chi middleware chains put handler last; scan every arg.
|
|
926
|
+
for (const arg of args) {
|
|
927
|
+
const expr = (arg.expression ?? arg.variable ?? arg.value ?? '').trim();
|
|
928
|
+
if (!expr)
|
|
929
|
+
continue;
|
|
930
|
+
// Strip package qualifier (`pkg.Handler` → `Handler`).
|
|
931
|
+
const short = expr.slice(expr.lastIndexOf('.') + 1);
|
|
932
|
+
if (short === method.name)
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
function goRegistrarReceiverMatches(receiver) {
|
|
939
|
+
const trimmed = receiver.trim();
|
|
940
|
+
if (!trimmed)
|
|
941
|
+
return false;
|
|
942
|
+
for (const target of GO_HTTP_REGISTRAR_RECEIVERS) {
|
|
943
|
+
if (trimmed === target)
|
|
944
|
+
return true;
|
|
945
|
+
// Method-chain suffix (`app.Group("/api")` → subsequent receiver
|
|
946
|
+
// is a Group value that still holds a router; be permissive).
|
|
947
|
+
if (trimmed.endsWith(`.${target}`))
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
// Bash classifier
|
|
954
|
+
// ---------------------------------------------------------------------------
|
|
955
|
+
/**
|
|
956
|
+
* Positional-parameter tokens whose presence in the script body
|
|
957
|
+
* indicates the script consumes command-line arguments — a real
|
|
958
|
+
* entry point from the OS's perspective.
|
|
959
|
+
*/
|
|
960
|
+
const BASH_POSITIONAL_TOKENS = [
|
|
961
|
+
'$1', '$2', '$3', '$4', '$5', '$6', '$7', '$8', '$9',
|
|
962
|
+
'$@', '$*', '$#',
|
|
963
|
+
'${1', '${2', '${3', '${@',
|
|
964
|
+
'getopts',
|
|
965
|
+
];
|
|
966
|
+
/**
|
|
967
|
+
* Filename prefixes that mark a script as a benign / safe fixture
|
|
968
|
+
* (per the ticket's `benign_*.sh` corpus convention) or a helper
|
|
969
|
+
* library that is `source`d from another script.
|
|
970
|
+
*/
|
|
971
|
+
const BASH_LIBRARY_FILENAME_PREFIXES = [
|
|
972
|
+
'benign_',
|
|
973
|
+
'safe_',
|
|
974
|
+
'lib_',
|
|
975
|
+
'common_',
|
|
976
|
+
'helpers_',
|
|
977
|
+
'_',
|
|
978
|
+
];
|
|
979
|
+
function classifyBashEntryPoint(method, enclosingType, ctx) {
|
|
980
|
+
// 1. Library / test / vendored path → TIER_3.
|
|
981
|
+
if (pathLooksLikeLibraryOrTest(ctx.filePath)) {
|
|
982
|
+
return 'TIER_3_LIBRARY_API';
|
|
983
|
+
}
|
|
984
|
+
// 2. Benign / safe / library filename prefix → TIER_3.
|
|
985
|
+
if (bashFilenameLooksLikeLibrary(ctx.filePath)) {
|
|
986
|
+
return 'TIER_3_LIBRARY_API';
|
|
987
|
+
}
|
|
988
|
+
// 3. `main()` function → TIER_1. Bash convention: script-body
|
|
989
|
+
// dispatches to `main "$@"`.
|
|
990
|
+
if (method.name === 'main') {
|
|
991
|
+
return 'TIER_1_ENTRY_POINT';
|
|
992
|
+
}
|
|
993
|
+
// 4. Positional-parameter use scan — walk `ir.calls` in this
|
|
994
|
+
// method's line range and look for `$1` / `$@` / `getopts` in
|
|
995
|
+
// argument text. If the method reads positional args, it IS
|
|
996
|
+
// the entry point.
|
|
997
|
+
if (methodConsumesPositionalArgs(method, ctx.calls)) {
|
|
998
|
+
return 'TIER_1_ENTRY_POINT';
|
|
999
|
+
}
|
|
1000
|
+
// 5. Script-body top-level → the enclosing "method" for Bash is
|
|
1001
|
+
// typically the file-level statement block. If we're in the
|
|
1002
|
+
// module-scope container and the file has ANY positional-arg
|
|
1003
|
+
// use, treat as TIER_1.
|
|
1004
|
+
if (enclosingType && looksLikeModuleType(enclosingType) && fileHasPositionalArgUse(ctx.calls)) {
|
|
1005
|
+
return 'TIER_1_ENTRY_POINT';
|
|
1006
|
+
}
|
|
1007
|
+
// 6. Fallback — UNKNOWN keeps recall.
|
|
1008
|
+
return 'TIER_UNKNOWN';
|
|
1009
|
+
}
|
|
1010
|
+
function bashFilenameLooksLikeLibrary(filePath) {
|
|
1011
|
+
if (!filePath)
|
|
1012
|
+
return false;
|
|
1013
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
1014
|
+
const slash = normalized.lastIndexOf('/');
|
|
1015
|
+
const base = (slash >= 0 ? normalized.slice(slash + 1) : normalized).toLowerCase();
|
|
1016
|
+
for (const prefix of BASH_LIBRARY_FILENAME_PREFIXES) {
|
|
1017
|
+
if (base.startsWith(prefix))
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
// `.test.sh` / `_test.sh` — test scripts.
|
|
1021
|
+
if (base.includes('.test.') || base.includes('_test.'))
|
|
1022
|
+
return true;
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
function methodConsumesPositionalArgs(method, calls) {
|
|
1026
|
+
if (!calls || calls.length === 0)
|
|
1027
|
+
return false;
|
|
1028
|
+
for (const call of calls) {
|
|
1029
|
+
if (call.location.line < method.start_line)
|
|
1030
|
+
continue;
|
|
1031
|
+
if (call.location.line > method.end_line)
|
|
1032
|
+
continue;
|
|
1033
|
+
if (callHasPositionalToken(call))
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
function fileHasPositionalArgUse(calls) {
|
|
1039
|
+
if (!calls || calls.length === 0)
|
|
1040
|
+
return false;
|
|
1041
|
+
for (const call of calls) {
|
|
1042
|
+
if (callHasPositionalToken(call))
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
function callHasPositionalToken(call) {
|
|
1048
|
+
// Check argument text for `$1` / `$@` / `getopts`.
|
|
1049
|
+
for (const arg of call.arguments ?? []) {
|
|
1050
|
+
const expr = arg.expression ?? arg.variable ?? arg.value ?? '';
|
|
1051
|
+
for (const tok of BASH_POSITIONAL_TOKENS) {
|
|
1052
|
+
if (expr.includes(tok))
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Also check method_name — `getopts` shows up as a call.
|
|
1057
|
+
if (call.method_name === 'getopts')
|
|
1058
|
+
return true;
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
// ---------------------------------------------------------------------------
|
|
1062
|
+
// Shared runtime-registration handler lookup
|
|
1063
|
+
// ---------------------------------------------------------------------------
|
|
1064
|
+
/**
|
|
1065
|
+
* Returns true when `method` is the named handler for any HTTP-route
|
|
1066
|
+
* / event-listener registration recorded in
|
|
1067
|
+
* `ctx.runtimeRegistrations`. Handler name match is exact — inline
|
|
1068
|
+
* arrow / anonymous handlers (`handler.name === null`) do not
|
|
1069
|
+
* match any named method and are ignored.
|
|
1070
|
+
*/
|
|
1071
|
+
function methodIsRuntimeRegistrationHandler(method, regs) {
|
|
1072
|
+
if (!regs || regs.length === 0)
|
|
1073
|
+
return false;
|
|
1074
|
+
for (const reg of regs) {
|
|
1075
|
+
// Only http_route / decorator / event_listener kinds mark a method
|
|
1076
|
+
// as an entry point. `trait_impl` is a Rust-only concept and
|
|
1077
|
+
// `middleware` alone is weak (framework middlewares often wrap
|
|
1078
|
+
// library code) — include middleware because our engine treats it
|
|
1079
|
+
// as a boundary.
|
|
1080
|
+
if (reg.kind !== 'http_route' &&
|
|
1081
|
+
reg.kind !== 'decorator' &&
|
|
1082
|
+
reg.kind !== 'event_listener' &&
|
|
1083
|
+
reg.kind !== 'middleware') {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const handlerName = reg.handler?.name;
|
|
1087
|
+
if (!handlerName)
|
|
1088
|
+
continue;
|
|
1089
|
+
if (handlerName === method.name)
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
488
1094
|
//# sourceMappingURL=entry-point-detection.js.map
|