circle-ir 3.165.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.
@@ -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
- // 2. Library-facade short-circuit. Runs BEFORE Tier-1 annotation
441
- // detectionsee `classShapeIsLibraryFacade` for the trade-off.
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
- // 3. Method-level annotation
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
- // 4. Class-level annotation (every public method of a controller is TIER_1)
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
- // 5. Supertype lifecycle method
483
+ // 4. Supertype lifecycle method
454
484
  if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
455
485
  return 'TIER_1_ENTRY_POINT';
456
486
  }
457
- // 6. `public static void main(String[])`
487
+ // 5. `public static void main(String[])`
458
488
  if (looksLikeMainMethod(method)) {
459
489
  return 'TIER_1_ENTRY_POINT';
460
490
  }
461
- // 7. Tier 2 reachability — deferred. See header doc.
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