ucn 5.4.2 → 5.5.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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/cache.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
12
|
const {
|
|
13
|
-
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, DEFAULT_IGNORES,
|
|
13
|
+
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing, DEFAULT_IGNORES,
|
|
14
14
|
classifyUnsupportedSourceFile,
|
|
15
15
|
} = require('./discovery');
|
|
16
16
|
const { codeUnitCompare } = require('./shared');
|
|
@@ -26,9 +26,21 @@ const CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
|
26
26
|
const CACHE_MAX_PROJECTS = 128;
|
|
27
27
|
const CACHE_MAX_BYTES = 1024 * 1024 * 1024;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Fingerprint of what decides the discovered file set besides the tree
|
|
31
|
+
* itself: the compiled .gitignore rules and whether git could list the
|
|
32
|
+
* tracked paths (a failed listing changes which files rules may exclude).
|
|
33
|
+
*/
|
|
34
|
+
function discoveryFingerprint(rules, listing) {
|
|
35
|
+
return crypto.createHash('md5')
|
|
36
|
+
.update(rules.fingerprint())
|
|
37
|
+
.update(listing?.failure ? '\0git-listing-failed' : '')
|
|
38
|
+
.digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function discoveryRulesHash(root) {
|
|
42
|
+
const listing = gitListing(root);
|
|
43
|
+
return discoveryFingerprint(parseGitignore(root, listing), listing);
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/**
|
|
@@ -714,7 +726,199 @@ function clearAllCaches() {
|
|
|
714
726
|
// v226: bundled/minified filename exclusions are disclosed in discoveryIssues.
|
|
715
727
|
// v227: signature parameter/return text excludes AST comments in every language.
|
|
716
728
|
// v228: Python keyword arguments retain the same callable-reference facts as positional arguments.
|
|
717
|
-
|
|
729
|
+
// v229 (fix #359): subscripts of declared containers type their element
|
|
730
|
+
// receivers (Python dict/list/Sequence, TS Array/Record/index signatures and
|
|
731
|
+
// `const c = xs[i]`, Java/C/C++ arrays, C# BCL indexers, C++ std containers,
|
|
732
|
+
// Rust Vec/HashMap/arrays).
|
|
733
|
+
// v230 (fix #360): Go embedded-field symbols carry `embedded`; Go files carry
|
|
734
|
+
// typeConversions (concrete values flowing into differently-typed slots);
|
|
735
|
+
// Rust `fn name(...)` inside macro token trees is no longer a call record.
|
|
736
|
+
// v231 (fix #361): C/C++ recovery keeps include-guarded bodies and
|
|
737
|
+
// access-specifier/statement macros inside class bodies; C++ macros carry
|
|
738
|
+
// namespaceScope, C++ files carry macroScopeMarkers and cppUsings, C++ path
|
|
739
|
+
// calls carry qualifierTemplateParam; angle-bracket project includes resolve
|
|
740
|
+
// through include directories and basename-resolved quoted includes are
|
|
741
|
+
// recorded in fileEntry.includeFallback.
|
|
742
|
+
// v232 (fix #362): C/C++ function-like macros carry their replacement-list
|
|
743
|
+
// template (ppParams/ppVariadic/ppBody); `#define`/`#include` directives the
|
|
744
|
+
// grammar places inside enumerator lists or initializers are indexed; the
|
|
745
|
+
// index cache carries per-file macro-expansion records (`macroExpansion`).
|
|
746
|
+
// v233 (fix #363): file entries carry `reflectionSites` (reflective member
|
|
747
|
+
// accesses: literal names, name patterns built from literal fragments,
|
|
748
|
+
// receiver facts), extracted from each file's own parse at index time.
|
|
749
|
+
// v234 (fix #364): Python functions whose own body yields and C# iterator
|
|
750
|
+
// methods (`yield return`) carry `isGenerator`, so audit-async classifies
|
|
751
|
+
// async generators as async iterators rather than coroutines.
|
|
752
|
+
// v235 (fix #366): JS/TS and Python call records carry `requestConfig`
|
|
753
|
+
// (object-literal / keyword `url`/`path` request configuration), C#
|
|
754
|
+
// attributes keep non-literal `args`, and Python absolute imports resolve
|
|
755
|
+
// against nested source roots (moduleResolved/importGraph content changed).
|
|
756
|
+
// v236 (fix #367): C/C++ definitions and calls inside `__cplusplus` blocks
|
|
757
|
+
// the file's own language skips are indexed with `languageBranch`.
|
|
758
|
+
// v238 (fix #369): Rust path calls carry `returnPosition` (the value the
|
|
759
|
+
// enclosing fn returns) and aliased `extern crate x as y;` declarations
|
|
760
|
+
// become import bindings.
|
|
761
|
+
// v239 (fix #370): Rust functions, methods and type aliases carry
|
|
762
|
+
// `futureReturn`; Rust qualifiers (async/unsafe/const/extern) come from the
|
|
763
|
+
// AST, and trait `async fn` members carry isAsync.
|
|
764
|
+
// v242 (fix #372): the index carries `cargoManifests` (the workspace crate
|
|
765
|
+
// registry's manifest snapshot, stat-validated for staleness).
|
|
766
|
+
// v243 (fix #372): Rust call records mark every value that flows on or is
|
|
767
|
+
// read from its local (`valueConsumed` 'flow'/'stored'), and a method taken
|
|
768
|
+
// on a stored value as `consumingMethod`; file entries carry `openCalls`
|
|
769
|
+
// (call shapes, replacing openCallNames); large directories' calls shards
|
|
770
|
+
// are split into path buckets.
|
|
771
|
+
// v244 (fix #374): Rust files invoking project macro_rules! carry
|
|
772
|
+
// `rustMacroExpansion` (expansion record, revalidated per build); their
|
|
773
|
+
// symbols and call records are those of the expanded file, and facts
|
|
774
|
+
// produced inside an invocation carry `macroExpansion`.
|
|
775
|
+
// v245 (fix #375): calls shards store each file's enclosing-function,
|
|
776
|
+
// macro-expansion and receiver-evidence objects once (`fx` / `mx` / `rx`
|
|
777
|
+
// tables, referenced by position).
|
|
778
|
+
// v246 (fix #377): Rust `mod` declarations resolve by the module layout
|
|
779
|
+
// rules (foo.rs + foo/ children, #[path], inline-module nesting); persisted
|
|
780
|
+
// moduleResolved/importGraph content changed.
|
|
781
|
+
// v247 (fix #378): Go generic instantiation calls (`f[T](x)`, `pkg.F[T](x)`)
|
|
782
|
+
// are call records, generic receivers/parameters are typed by their base
|
|
783
|
+
// type, Go aliases record `aliasOf` for generic bases and `aliasQualifier`
|
|
784
|
+
// for package-qualified ones, package-qualified function values are function
|
|
785
|
+
// references, and function-local Go types and JS classes carry their lexical
|
|
786
|
+
// scope.
|
|
787
|
+
// v248 (fix #379): C/C++ recovery blanks a decoration macro before a second
|
|
788
|
+
// type specifier and in `class EXPORT Name : bases` heads, member-list macro
|
|
789
|
+
// invocations carry their access effect (`accessAfterMacro`), and functions
|
|
790
|
+
// under a preprocessor conditional record `ppBranch`.
|
|
791
|
+
// v249 (fix #380): C# type definitions carry `typeArity` and their members
|
|
792
|
+
// `ownerTypeArity`; call records carry the written type-argument count of
|
|
793
|
+
// constructors (`typeArgs`), type-qualified and typed receivers
|
|
794
|
+
// (`receiverTypeArgs`, `receiverRootTypeArgs`); generic type receivers
|
|
795
|
+
// (`Outcome<int>.Create()`) are type-qualified; Java `implements` keeps
|
|
796
|
+
// qualified supertypes.
|
|
797
|
+
// v250 (fix #381): Python function-local classes carry their lexical scope,
|
|
798
|
+
// and Python/JS/Java/C#/Go/Rust call records on a one-hop local alias of a
|
|
799
|
+
// field path or typed local receive like the aliased expression.
|
|
800
|
+
// v251 (fix #383): route registrations carry `handlerArgs` (JS/TS, Go),
|
|
801
|
+
// Go method-first registrations (`Handle(method, path, h)`)
|
|
802
|
+
// `methodFirstRoute`, Fastify route-config objects their `handler`, Go request constructors
|
|
803
|
+
// `requestTarget` { method, url }, and axum `.route()` calls `methodRouter`;
|
|
804
|
+
// Go file entries carry `packageName` (the package clause).
|
|
805
|
+
// v252 (fix #384): JS/TS member-assigned functions carry `assignedObject`
|
|
806
|
+
// ('global' | 'object') and `selfNamed` (bare-name reachability).
|
|
807
|
+
// v253 (fix #385): C/C++ members inside preprocessor conditionals of a class
|
|
808
|
+
// body are indexed (with `ppBranch`), invocations of a file's own
|
|
809
|
+
// function-like macros read as declarations are recovered (members they
|
|
810
|
+
// declare carry `generatedByMacro`), and macro invocations read as functions
|
|
811
|
+
// are not indexed.
|
|
812
|
+
// v254 (fix #386): specs of a grouped Go `type ( ... )` declaration carry
|
|
813
|
+
// `nameLine` (the line naming the type).
|
|
814
|
+
// v255 (fix #387): C/C++ recovery blanks declaration-specifier macros, never
|
|
815
|
+
// the type, scope or keyword next to them (return types, members and line
|
|
816
|
+
// numbers of recovered declarations changed); C/C++ file entries carry
|
|
817
|
+
// `recoveryBlanks` (the byte ranges the recovery blanked).
|
|
818
|
+
// v256 (fix #389): Java annotation types (`@interface`, `annotationType`) with
|
|
819
|
+
// their elements and interface constants, Java type `nameLine`, Rust unions,
|
|
820
|
+
// struct/variant `valueShape` ('unit' | 'tuple'), C/C++ function-local class
|
|
821
|
+
// scopes, file-level `moduleValueAliases` (Python/JS/TS), Rust call records'
|
|
822
|
+
// `receiverValuePath` for unbound identifiers / struct expressions / one-hop
|
|
823
|
+
// locals (`receiverValueKind`), Java annotation element call records
|
|
824
|
+
// (`annotationElement`, `implicitName`), C# typeof reflection `arity`.
|
|
825
|
+
// v257 (fix #390): C# declarations carry `nameLine` when attributes start
|
|
826
|
+
// them and `generics` (type parameter names) on generic types, methods and
|
|
827
|
+
// local functions; C++ class templates carry `generics`; Java fields and
|
|
828
|
+
// TS/JS decorated fields carry `nameLine`; Java/C# call records read a `+`
|
|
829
|
+
// with a string operand as a `string` argument.
|
|
830
|
+
// v259 (fix #391): C++ friend functions are namespace-scope functions
|
|
831
|
+
// (`friendOf` names the befriending class) instead of members, and member
|
|
832
|
+
// templates carry their return type and `templateDependent`; namespace-scope
|
|
833
|
+
// macro invocations followed by a body are generated callables
|
|
834
|
+
// (`generatedByMacro`); C# files whose `#if` splits a declaration are read
|
|
835
|
+
// through configuration views, recorded per file as `conditionalViews`;
|
|
836
|
+
// C# explicit interface properties, indexers and events carry
|
|
837
|
+
// `explicitInterface`; large C/C++ files resolve the conditionals the
|
|
838
|
+
// grammar could not place; C#/Java call records carry explicit method type
|
|
839
|
+
// arguments (`methodTypeArgs`) and lambda parameter counts (`lambda:N`), and
|
|
840
|
+
// C# argument kinds type `params` parameters, fields of the enclosing type
|
|
841
|
+
// and array creations by rank.
|
|
842
|
+
// v260 (fix #392): Rust top-level const/static items carry their declared
|
|
843
|
+
// type (`valueType`); Python module aliases bound on several module-level
|
|
844
|
+
// paths carry `alternatives`, JS/TS `let`/`var` aliases never reassigned are
|
|
845
|
+
// aliases; Java enums record `implements` and their methods' `nameLine` /
|
|
846
|
+
// annotations; Rust call records on locals destructured from `self` carry
|
|
847
|
+
// the field-hop receiver; Python usage records classify default values as
|
|
848
|
+
// references and bare/lambda parameters as definitions (plan's usage mode
|
|
849
|
+
// adds each bare reference's lexical scope verdict).
|
|
850
|
+
// v261 (fix #393): C/C++ unnamed parameters carry `unnamed` with their type
|
|
851
|
+
// as the name; C++ `v->m()` call records carry `receiverArrow` (and the
|
|
852
|
+
// declared object type `receiverArrowObject` when `v` is an object); C++
|
|
853
|
+
// function templates carry `templateParams`, member templates with a body
|
|
854
|
+
// are definitions, members in skipped conditional branches are indexed,
|
|
855
|
+
// namespaces left as ERROR tokens or closed early by the parse scope their
|
|
856
|
+
// declarations, large files resolve brace-splitting conditionals, pointer
|
|
857
|
+
// arguments keep their pointer level in `type:` argument kinds; C#
|
|
858
|
+
// delegates carry `delegateParams` and target-typed `new(..)` records a
|
|
859
|
+
// constructor call (`targetTyped`).
|
|
860
|
+
// v262 (fix #394): Java argument kinds keep array dimensions
|
|
861
|
+
// (`type:java.lang.String[]`, `new:..[]`, `cast:..[]`) and receivers
|
|
862
|
+
// declared with a package-qualified type carry the package as
|
|
863
|
+
// `receiverTypeQualifier`.
|
|
864
|
+
// v263 (fix #395): C# declarations carry their full namespace (every
|
|
865
|
+
// enclosing `namespace` block), C# using directives record the namespace
|
|
866
|
+
// they sit in, `using static` and `global using`.
|
|
867
|
+
// v264 (fix #396): C/C++ anonymous typedef'd specifiers record
|
|
868
|
+
// `typedefName`, nested classes (in-class and `struct Outer::Inner`)
|
|
869
|
+
// `enclosingType`, generated callables their identifier arguments
|
|
870
|
+
// (`generatedByMacro.args`) and pasted member-list names `unspelled`; calls
|
|
871
|
+
// on `#define` continuation lines are replacement-list calls; recovery blanks
|
|
872
|
+
// decoration macros before parenthesized names and return-type macro
|
|
873
|
+
// invocations; files record the project macro names their recovery used
|
|
874
|
+
// (`externalMacroNames`, `externalMacroKey`, `recoveryCandidates`).
|
|
875
|
+
// v265 (fix #396): a replacement-list receiver declared with a macro
|
|
876
|
+
// parameter as its type carries no receiver type.
|
|
877
|
+
// v266 (fix #396): a class body the grammar closed early is never read as
|
|
878
|
+
// the return type of the next member (no decoration blank there).
|
|
879
|
+
// v267 (fix #397): JS/TS bare records of names an object pattern binds carry
|
|
880
|
+
// `destructured` (property key, position, source receiver facts); local
|
|
881
|
+
// shadows carry the shadowing declaration's line as `localShadow` (-1 for a
|
|
882
|
+
// parameter) and plain calls carry them too; stored methods record `memberValue` references;
|
|
883
|
+
// `require('m').y` binds y under the local name; object-literal members
|
|
884
|
+
// carry `objectLiteralLine` (accessors their get/set kind); usages mark
|
|
885
|
+
// shorthand properties and import aliases; the index persists the
|
|
886
|
+
// in-repository JS packages (`jsWorkspacePackages`); JS call records carry
|
|
887
|
+
// `uncertain` only when true.
|
|
888
|
+
// v268 (fix #398): repeated import source names retain every local binding;
|
|
889
|
+
// an alias record applies to one specifier, not all same-name specifiers.
|
|
890
|
+
// v269 (fix #398F): Python files record `configurationItems` (names bound in
|
|
891
|
+
// one scope only in exclusive `if`/`try`/`match` branches).
|
|
892
|
+
// v270 (fix #399): Go files record `packageVars` (package-level variables
|
|
893
|
+
// with their declared static types) and their `buildConstraint`; Go method
|
|
894
|
+
// calls on a receiver no scope of the file binds carry `receiverPackageScope`.
|
|
895
|
+
// v271 (fix #400): Go imports that write no local name are marked
|
|
896
|
+
// `implicitName` (a project import's local name is its package clause) and
|
|
897
|
+
// carry the name the Go tools suggest (`go-json` is `json`, `core/v1` `v1`);
|
|
898
|
+
// `packageVars` also record constants and `valueFrom` initializers, and
|
|
899
|
+
// untyped package-level variables of the file carry `receiverPackageScope`.
|
|
900
|
+
// C# target-typed `new(..)` arguments and member assignments are `new`
|
|
901
|
+
// records with a `targetSlot` (files list their `targetTypedKeys`), C# types
|
|
902
|
+
// record `primaryParams`; Python bound-method reads assigned to a name
|
|
903
|
+
// (`loads = registry.loads`) are method-value records.
|
|
904
|
+
// v272 (fix #401): Rust receivers typed through std deref wrappers carry the
|
|
905
|
+
// target type with the wrapper chain on their evidence (`derefVia`), statics
|
|
906
|
+
// record `valueDerefVia`, array literal receivers are 'array'; C++ subscript
|
|
907
|
+
// receivers of class objects record `receiverSubscriptObject`, macro-body
|
|
908
|
+
// receivers declared with a macro parameter `receiverMacroParam`, and
|
|
909
|
+
// `T v(X());` initializer calls are `declarationReading` records; C#
|
|
910
|
+
// target-typed `new(..)` collection elements are typed, argument slots of
|
|
911
|
+
// generic receivers carry `receiverTypeText` (a `~` targetTypedKeys entry);
|
|
912
|
+
// Java `super` calls in enum-constant bodies carry `receiverSuperType`.
|
|
913
|
+
// v273 (fix #402): Python defs under compound statements of a class body
|
|
914
|
+
// are methods of the class and `Union[X, None]` annotations name X; JS/TS
|
|
915
|
+
// anonymous default exports and member-assigned functions record `isAsync`,
|
|
916
|
+
// a default import beside named or namespace imports is its own `default`
|
|
917
|
+
// import record; JS/TS and Python member-call records mark `awaited` and
|
|
918
|
+
// `passedAsArgument`.
|
|
919
|
+
// v274 (fix #403): a Python reference inside a lambda whose enclosing def
|
|
920
|
+
// binds the name records that def as its `localShadowScope`.
|
|
921
|
+
const CACHE_FORMAT_VERSION = 274;
|
|
718
922
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
719
923
|
|
|
720
924
|
/**
|
|
@@ -815,21 +1019,25 @@ function saveCache(index, cachePath) {
|
|
|
815
1019
|
|
|
816
1020
|
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
817
1021
|
|
|
1022
|
+
// A partially loaded (per-file) calls cache must be complete before it
|
|
1023
|
+
// is rewritten: the shard directory is replaced wholesale below.
|
|
1024
|
+
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
1025
|
+
ensureCallsCacheLoaded(index);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
818
1028
|
// Prepare callsCache for serialization (exclude content, use relative paths)
|
|
819
1029
|
const callsCacheData = [];
|
|
820
1030
|
for (const [filePath, entry] of index.callsCache) {
|
|
821
|
-
callsCacheData.push([path.relative(index.root, filePath),
|
|
822
|
-
mtime: entry.mtime,
|
|
823
|
-
hash: entry.hash,
|
|
824
|
-
calls: entry.calls
|
|
825
|
-
// content is not persisted - will be read on demand
|
|
826
|
-
}]);
|
|
1031
|
+
callsCacheData.push([path.relative(index.root, filePath), entry]);
|
|
827
1032
|
}
|
|
828
1033
|
|
|
829
1034
|
// Hash config to detect when graph rebuild is needed on load
|
|
830
1035
|
const configHash = crypto.createHash('md5')
|
|
831
1036
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
832
|
-
|
|
1037
|
+
// The rules the index was discovered with (fix #382: re-asking git at
|
|
1038
|
+
// save time cost a second listing and could describe newer rules).
|
|
1039
|
+
const discoveryHash = index._discoveryHash || index._loadedDiscoveryHash ||
|
|
1040
|
+
discoveryRulesHash(index.root);
|
|
833
1041
|
|
|
834
1042
|
// Strip redundant fields from symbols and file entries to reduce cache size.
|
|
835
1043
|
// v6: All paths stored as relative paths (saves ~60% on large codebases).
|
|
@@ -938,6 +1146,20 @@ function saveCache(index, cachePath) {
|
|
|
938
1146
|
unsupportedFiles: Array.isArray(index.unsupportedFiles)
|
|
939
1147
|
? index.unsupportedFiles
|
|
940
1148
|
: [],
|
|
1149
|
+
...(Array.isArray(index.cargoManifests) && {
|
|
1150
|
+
cargoManifests: index.cargoManifests,
|
|
1151
|
+
cargoManifestsSeen: index.cargoManifestsSeen || null,
|
|
1152
|
+
}),
|
|
1153
|
+
// In-repository JS packages (fix #397), so a cache-loaded query
|
|
1154
|
+
// never re-reads the manifests.
|
|
1155
|
+
// Names and directories only; a manifest is read again when an
|
|
1156
|
+
// import resolves through it.
|
|
1157
|
+
...(index._jsWorkspacePackages !== undefined && {
|
|
1158
|
+
jsWorkspacePackages: index._jsWorkspacePackages
|
|
1159
|
+
? [...index._jsWorkspacePackages].map(([name, pkg]) =>
|
|
1160
|
+
[name, pkg.ambiguous ? null : path.relative(root, pkg.dir)])
|
|
1161
|
+
: null,
|
|
1162
|
+
}),
|
|
941
1163
|
includeBundled: index.includeBundled === true,
|
|
942
1164
|
discoveryIssues: Array.isArray(index.discoveryIssues)
|
|
943
1165
|
? index.discoveryIssues
|
|
@@ -947,6 +1169,12 @@ function saveCache(index, cachePath) {
|
|
|
947
1169
|
reachableSymbols: reachableSymbolsRel,
|
|
948
1170
|
reachableFingerprint,
|
|
949
1171
|
}),
|
|
1172
|
+
// C/C++ macro-expansion records (fix #362), revalidated per file on use.
|
|
1173
|
+
...(() => {
|
|
1174
|
+
const { persistableMacroExpansion } = require('./macro-expansion');
|
|
1175
|
+
const macroExpansion = persistableMacroExpansion(index);
|
|
1176
|
+
return macroExpansion ? { macroExpansion } : {};
|
|
1177
|
+
})(),
|
|
950
1178
|
...(index._computedDispatchBlindspots instanceof Map && {
|
|
951
1179
|
computedDispatchBlindspots: [...index._computedDispatchBlindspots]
|
|
952
1180
|
.map(([filePath, sites]) => [path.relative(root, filePath), sites]),
|
|
@@ -966,6 +1194,7 @@ function saveCache(index, cachePath) {
|
|
|
966
1194
|
index.reachabilityDirty = false;
|
|
967
1195
|
}
|
|
968
1196
|
index.computedDispatchDirty = false;
|
|
1197
|
+
index.macroExpansionDirty = false;
|
|
969
1198
|
|
|
970
1199
|
// Save callsCache sharded by directory for lazy loading.
|
|
971
1200
|
// Write to a temp directory first, then atomic swap to avoid data loss on crash.
|
|
@@ -988,13 +1217,26 @@ function saveCache(index, cachePath) {
|
|
|
988
1217
|
shards.get(dir).push([relPath, entry]);
|
|
989
1218
|
}
|
|
990
1219
|
|
|
991
|
-
// Write all shards to temp directory
|
|
1220
|
+
// Write all shards to temp directory. A large directory is split
|
|
1221
|
+
// into buckets by file path (fix #372), so a per-file consumer loads
|
|
1222
|
+
// roughly that file's records instead of its whole directory.
|
|
992
1223
|
const shardManifest = [];
|
|
993
1224
|
for (const [dir, entries] of shards) {
|
|
994
1225
|
const hash = crypto.createHash('md5').update(dir).digest('hex').slice(0, 10);
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
1226
|
+
const serialized = entries.map(([relPath, entry]) => serializeCallsEntry(relPath, entry));
|
|
1227
|
+
const bytes = serialized.reduce((total, text) => total + text.length + 1, 0);
|
|
1228
|
+
const buckets = Math.max(1, Math.min(entries.length, Math.ceil(bytes / CALLS_SHARD_TARGET_BYTES)));
|
|
1229
|
+
if (buckets === 1) {
|
|
1230
|
+
fs.writeFileSync(path.join(callsTmpDir, `${hash}.json`), `[${serialized.join(',')}]`);
|
|
1231
|
+
shardManifest.push([dir, hash, entries.length]);
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const parts = Array.from({ length: buckets }, () => []);
|
|
1235
|
+
entries.forEach(([relPath], i) => parts[callsShardBucket(relPath, buckets)].push(serialized[i]));
|
|
1236
|
+
parts.forEach((part, k) => {
|
|
1237
|
+
fs.writeFileSync(path.join(callsTmpDir, `${hash}.${k}.json`), `[${part.join(',')}]`);
|
|
1238
|
+
});
|
|
1239
|
+
shardManifest.push([dir, hash, entries.length, buckets]);
|
|
998
1240
|
}
|
|
999
1241
|
|
|
1000
1242
|
// Write manifest to temp directory
|
|
@@ -1084,6 +1326,14 @@ function loadCache(index, cachePath) {
|
|
|
1084
1326
|
index._groundSetCacheLines = 0;
|
|
1085
1327
|
index._nameBindingReachCache = new Map();
|
|
1086
1328
|
index._returnTypeFlowCache = new Map();
|
|
1329
|
+
index._classIdentity = null;
|
|
1330
|
+
index._macroExpansion = null;
|
|
1331
|
+
index._macroSpellable = null; // fix #385: paste-spellability memo over the same symbols
|
|
1332
|
+
index._rustMacroState = null;
|
|
1333
|
+
index._cppScope = null;
|
|
1334
|
+
index._contractIndex = null;
|
|
1335
|
+
index._typeDenotation = null;
|
|
1336
|
+
index._languageFeatureMemo = null; // fix #380 protocol-type feature memo
|
|
1087
1337
|
|
|
1088
1338
|
// Reconstruct files Map: relative key → absolute key, restore path and relativePath
|
|
1089
1339
|
// Initialize symbols/bindings arrays (will be populated from top-level symbols)
|
|
@@ -1168,6 +1418,22 @@ function loadCache(index, cachePath) {
|
|
|
1168
1418
|
index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
|
|
1169
1419
|
? cacheData.unsupportedFiles
|
|
1170
1420
|
: [];
|
|
1421
|
+
// Cargo manifest snapshot (fix #372): seeds the workspace crate
|
|
1422
|
+
// registry so queries never walk the tree for it.
|
|
1423
|
+
index.cargoManifests = Array.isArray(cacheData.cargoManifests)
|
|
1424
|
+
? cacheData.cargoManifests
|
|
1425
|
+
: null;
|
|
1426
|
+
index.cargoManifestsSeen = cacheData.cargoManifestsSeen || null;
|
|
1427
|
+
if (index.cargoManifests) {
|
|
1428
|
+
require('./imports').seedWorkspaceManifests(index.root, index.cargoManifests);
|
|
1429
|
+
}
|
|
1430
|
+
if (cacheData.jsWorkspacePackages !== undefined) {
|
|
1431
|
+
index._jsWorkspacePackages = Array.isArray(cacheData.jsWorkspacePackages)
|
|
1432
|
+
? new Map(cacheData.jsWorkspacePackages.map(([name, dir]) => [name, typeof dir === 'string'
|
|
1433
|
+
? { name, dir: path.join(index.root, dir), workspace: true, manifestPending: true }
|
|
1434
|
+
: { ambiguous: true }]))
|
|
1435
|
+
: null;
|
|
1436
|
+
}
|
|
1171
1437
|
index.includeBundled = cacheData.includeBundled === true;
|
|
1172
1438
|
index.discoveryIssues = Array.isArray(cacheData.discoveryIssues)
|
|
1173
1439
|
? cacheData.discoveryIssues
|
|
@@ -1175,6 +1441,11 @@ function loadCache(index, cachePath) {
|
|
|
1175
1441
|
index.truncated = cacheData.truncated || null;
|
|
1176
1442
|
index._loadedConfigHash = cacheData.configHash || null;
|
|
1177
1443
|
index._loadedDiscoveryHash = cacheData.discoveryHash || null;
|
|
1444
|
+
index._discoveryHash = null;
|
|
1445
|
+
index._macroExpansionPersisted = cacheData.macroExpansion &&
|
|
1446
|
+
typeof cacheData.macroExpansion.signature === 'string'
|
|
1447
|
+
? cacheData.macroExpansion : null;
|
|
1448
|
+
index.macroExpansionDirty = false;
|
|
1178
1449
|
if (Array.isArray(cacheData.computedDispatchBlindspots)) {
|
|
1179
1450
|
index._computedDispatchBlindspots = new Map(
|
|
1180
1451
|
cacheData.computedDispatchBlindspots.map(([relPath, sites]) => [
|
|
@@ -1184,6 +1455,7 @@ function loadCache(index, cachePath) {
|
|
|
1184
1455
|
);
|
|
1185
1456
|
index.computedDispatchDirty = false;
|
|
1186
1457
|
}
|
|
1458
|
+
index._reflectionPatternIndex = null;
|
|
1187
1459
|
|
|
1188
1460
|
// Restore calleeIndex if persisted (v7 caches only; v8+ rebuilds lazily)
|
|
1189
1461
|
if (Array.isArray(cacheData.calleeIndex)) {
|
|
@@ -1253,11 +1525,13 @@ function isCacheStale(index) {
|
|
|
1253
1525
|
// new-file walk below. On a fresh cache these used to run two identical
|
|
1254
1526
|
// `git ls-files` subprocesses per one-shot CLI invocation, accounting for
|
|
1255
1527
|
// roughly a third of warm-start staleness time on measured repositories.
|
|
1256
|
-
let
|
|
1528
|
+
let gitignoreRules = null;
|
|
1529
|
+
// One git subprocess answers both questions discovery asks (fix #375).
|
|
1530
|
+
let listing;
|
|
1531
|
+
const gitListingOnce = () => (listing === undefined ? (listing = gitListing(index.root)) : listing);
|
|
1257
1532
|
if (index._loadedDiscoveryHash) {
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
index._loadedDiscoveryHash) {
|
|
1533
|
+
gitignoreRules = parseGitignore(index.root, gitListingOnce());
|
|
1534
|
+
if (discoveryFingerprint(gitignoreRules, listing) !== index._loadedDiscoveryHash) {
|
|
1261
1535
|
return true;
|
|
1262
1536
|
}
|
|
1263
1537
|
}
|
|
@@ -1296,6 +1570,12 @@ function isCacheStale(index) {
|
|
|
1296
1570
|
}
|
|
1297
1571
|
}
|
|
1298
1572
|
|
|
1573
|
+
// Cargo manifests steer Rust import resolution (fix #372).
|
|
1574
|
+
if (index.cargoManifests &&
|
|
1575
|
+
!require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests)) {
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1299
1579
|
// Ultra-fast skip for the SLOW path only: last confirmed-fresh < 2s ago
|
|
1300
1580
|
// (covers MCP burst calls). Uses _lastFreshAt (set at the end of a
|
|
1301
1581
|
// successful full check), never the cache save timestamp.
|
|
@@ -1308,6 +1588,7 @@ function isCacheStale(index) {
|
|
|
1308
1588
|
const pattern = detectProjectPattern(index.root);
|
|
1309
1589
|
const currentUnsupported = [];
|
|
1310
1590
|
const currentBundled = [];
|
|
1591
|
+
const currentManifests = [];
|
|
1311
1592
|
const globOpts = {
|
|
1312
1593
|
root: index.root,
|
|
1313
1594
|
includeBundled: index.includeBundled === true,
|
|
@@ -1315,6 +1596,9 @@ function isCacheStale(index) {
|
|
|
1315
1596
|
if (issue.reason === 'bundled') currentBundled.push(path.relative(index.root, issue.path));
|
|
1316
1597
|
},
|
|
1317
1598
|
onSkippedFile: (filePath) => {
|
|
1599
|
+
if (path.basename(filePath) === 'Cargo.toml') {
|
|
1600
|
+
currentManifests.push(path.relative(index.root, filePath));
|
|
1601
|
+
}
|
|
1318
1602
|
const kind = classifyUnsupportedSourceFile(filePath);
|
|
1319
1603
|
if (!kind) return;
|
|
1320
1604
|
currentUnsupported.push({
|
|
@@ -1323,9 +1607,9 @@ function isCacheStale(index) {
|
|
|
1323
1607
|
});
|
|
1324
1608
|
},
|
|
1325
1609
|
};
|
|
1326
|
-
if (!
|
|
1327
|
-
globOpts.
|
|
1328
|
-
globOpts.trackedPaths = gitTrackedPaths(index.root);
|
|
1610
|
+
if (!gitignoreRules) gitignoreRules = parseGitignore(index.root, gitListingOnce());
|
|
1611
|
+
globOpts.gitignoreRules = gitignoreRules;
|
|
1612
|
+
globOpts.trackedPaths = gitTrackedPaths(index.root, gitListingOnce());
|
|
1329
1613
|
const configExclude = index.config.exclude || [];
|
|
1330
1614
|
if (configExclude.length > 0) {
|
|
1331
1615
|
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
@@ -1363,6 +1647,12 @@ function isCacheStale(index) {
|
|
|
1363
1647
|
return true;
|
|
1364
1648
|
}
|
|
1365
1649
|
|
|
1650
|
+
if (index.cargoManifests &&
|
|
1651
|
+
!require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests,
|
|
1652
|
+
currentManifests, index.cargoManifestsSeen)) {
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1366
1656
|
// Record when we last confirmed the cache is fresh (enables 2s skip on burst calls)
|
|
1367
1657
|
index._lastFreshAt = Date.now();
|
|
1368
1658
|
return false;
|
|
@@ -1389,8 +1679,11 @@ function _prepareCallsCache(index, cacheFile) {
|
|
|
1389
1679
|
try {
|
|
1390
1680
|
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
1391
1681
|
index._callsManifest = new Map();
|
|
1392
|
-
for (const [dir, hash, count] of manifest) {
|
|
1393
|
-
index._callsManifest.set(dir, {
|
|
1682
|
+
for (const [dir, hash, count, buckets] of manifest) {
|
|
1683
|
+
index._callsManifest.set(dir, {
|
|
1684
|
+
hash, count, buckets: buckets || 1,
|
|
1685
|
+
loaded: false, loadedBuckets: buckets > 1 ? new Set() : null,
|
|
1686
|
+
});
|
|
1394
1687
|
}
|
|
1395
1688
|
index._callsCachePrepared = true;
|
|
1396
1689
|
return;
|
|
@@ -1418,10 +1711,19 @@ function loadCallsCache(index) {
|
|
|
1418
1711
|
if (index._callsCacheLoaded) return index.callsCache.size > 0;
|
|
1419
1712
|
index._callsCacheLoaded = true;
|
|
1420
1713
|
|
|
1421
|
-
// If manifest was prepared lazily, load all shards now
|
|
1714
|
+
// If manifest was prepared lazily, load all shards now (a shard already
|
|
1715
|
+
// loaded for a single file is not parsed twice).
|
|
1422
1716
|
if (index._callsManifest) {
|
|
1423
|
-
for (const [,
|
|
1424
|
-
|
|
1717
|
+
for (const [, shard] of index._callsManifest) {
|
|
1718
|
+
if (shard.loaded) continue;
|
|
1719
|
+
shard.loaded = true;
|
|
1720
|
+
if (shard.buckets > 1) {
|
|
1721
|
+
for (let k = 0; k < shard.buckets; k++) {
|
|
1722
|
+
if (!shard.loadedBuckets.has(k)) _loadCallsShard(index, `${shard.hash}.${k}`);
|
|
1723
|
+
}
|
|
1724
|
+
} else {
|
|
1725
|
+
_loadCallsShard(index, shard.hash);
|
|
1726
|
+
}
|
|
1425
1727
|
}
|
|
1426
1728
|
return index.callsCache.size > 0;
|
|
1427
1729
|
}
|
|
@@ -1438,7 +1740,7 @@ function loadCallsCache(index) {
|
|
|
1438
1740
|
if (!relPath || !entry) continue;
|
|
1439
1741
|
const absPath = path.isAbsolute(relPath) ? relPath : path.join(index.root, relPath);
|
|
1440
1742
|
if (!index.callsCache.has(absPath)) {
|
|
1441
|
-
index.callsCache.set(absPath, entry);
|
|
1743
|
+
index.callsCache.set(absPath, decodeCallsEntry(entry));
|
|
1442
1744
|
}
|
|
1443
1745
|
}
|
|
1444
1746
|
return index.callsCache.size > 0;
|
|
@@ -1460,6 +1762,194 @@ function ensureCallsCacheLoaded(index) {
|
|
|
1460
1762
|
}
|
|
1461
1763
|
}
|
|
1462
1764
|
|
|
1765
|
+
/**
|
|
1766
|
+
* Make one file's persisted call records available without loading every
|
|
1767
|
+
* shard (fix #365): a per-file consumer (audit-async's prefilter, single-file
|
|
1768
|
+
* queries) used to pay for parsing the whole sharded calls cache. Shards are
|
|
1769
|
+
* per directory, as saveCache writes them; whole-cache consumers still call
|
|
1770
|
+
* ensureCallsCacheLoaded, which loads the remaining shards.
|
|
1771
|
+
* @param {object} index - ProjectIndex instance
|
|
1772
|
+
* @param {string} filePath - Absolute file path
|
|
1773
|
+
*/
|
|
1774
|
+
function ensureCallsShardForFile(index, filePath) {
|
|
1775
|
+
if (!index._callsCachePrepared || index._callsCacheLoaded) return;
|
|
1776
|
+
if (!index._callsManifest) {
|
|
1777
|
+
loadCallsCache(index);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
const relPath = path.relative(index.root, filePath);
|
|
1781
|
+
const shard = index._callsManifest.get(path.dirname(relPath) || '.');
|
|
1782
|
+
if (!shard || shard.loaded) return;
|
|
1783
|
+
if (shard.buckets > 1) {
|
|
1784
|
+
const bucket = callsShardBucket(relPath, shard.buckets);
|
|
1785
|
+
if (shard.loadedBuckets.has(bucket)) return;
|
|
1786
|
+
shard.loadedBuckets.add(bucket);
|
|
1787
|
+
if (shard.loadedBuckets.size === shard.buckets) shard.loaded = true;
|
|
1788
|
+
_loadCallsShard(index, `${shard.hash}.${bucket}`);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
shard.loaded = true;
|
|
1792
|
+
_loadCallsShard(index, shard.hash);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Calls shards are split into buckets of about this many serialized bytes
|
|
1796
|
+
// (fix #372); small directories keep one shard.
|
|
1797
|
+
const CALLS_SHARD_TARGET_BYTES = 64 * 1024;
|
|
1798
|
+
|
|
1799
|
+
/** Stable bucket of a project-relative path among `buckets` (FNV-1a). */
|
|
1800
|
+
function callsShardBucket(relPath, buckets) {
|
|
1801
|
+
const key = String(relPath).split(path.sep).join('/');
|
|
1802
|
+
let h = 0x811c9dc5;
|
|
1803
|
+
for (let i = 0; i < key.length; i++) {
|
|
1804
|
+
h ^= key.charCodeAt(i);
|
|
1805
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
1806
|
+
}
|
|
1807
|
+
return h % buckets;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Record fields whose objects repeat within a file (the records of one
|
|
1812
|
+
* function share their `enclosingFunction`, those of one macro invocation
|
|
1813
|
+
* their `macroExpansion`, those of one typed local its receiver evidence),
|
|
1814
|
+
* persisted once per file in a table and referenced by position (fix #375:
|
|
1815
|
+
* they were a third of a large project's calls cache).
|
|
1816
|
+
*/
|
|
1817
|
+
const CALL_OBJECT_TABLES = [
|
|
1818
|
+
['enclosingFunction', 'fx'],
|
|
1819
|
+
['macroExpansion', 'mx'],
|
|
1820
|
+
['receiverTypeEvidence', 'rx'],
|
|
1821
|
+
['destructured', 'dx'],
|
|
1822
|
+
];
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* JSON text of `[relPath, entry]` for a calls shard, the repeated record
|
|
1826
|
+
* objects tabled (CALL_OBJECT_TABLES). The records are swapped in place
|
|
1827
|
+
* while serializing and restored.
|
|
1828
|
+
*/
|
|
1829
|
+
function serializeCallsEntry(relPath, entry) {
|
|
1830
|
+
const calls = entry.calls;
|
|
1831
|
+
const out = {
|
|
1832
|
+
mtime: entry.mtime,
|
|
1833
|
+
hash: entry.hash,
|
|
1834
|
+
calls,
|
|
1835
|
+
// content is not persisted - will be read on demand
|
|
1836
|
+
};
|
|
1837
|
+
if (!Array.isArray(calls)) return JSON.stringify([relPath, out]);
|
|
1838
|
+
const tables = CALL_OBJECT_TABLES.map(() => new SlotTable());
|
|
1839
|
+
const swapped = [];
|
|
1840
|
+
try {
|
|
1841
|
+
for (const call of calls) {
|
|
1842
|
+
for (let t = 0; t < CALL_OBJECT_TABLES.length; t++) {
|
|
1843
|
+
const field = CALL_OBJECT_TABLES[t][0];
|
|
1844
|
+
const value = call[field];
|
|
1845
|
+
if (value === null || typeof value !== 'object') continue;
|
|
1846
|
+
swapped.push(call, field, value);
|
|
1847
|
+
call[field] = tables[t].slot(value);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
CALL_OBJECT_TABLES.forEach(([, key], t) => {
|
|
1851
|
+
if (tables[t].values.length > 0) out[key] = tables[t].values;
|
|
1852
|
+
});
|
|
1853
|
+
return JSON.stringify([relPath, out]);
|
|
1854
|
+
} finally {
|
|
1855
|
+
for (let i = 0; i < swapped.length; i += 3) swapped[i][swapped[i + 1]] = swapped[i + 2];
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Distinct JSON values in first-seen order. Equal values (same keys in the
|
|
1861
|
+
* same order, same values) share a slot. Candidates are bucketed by name and
|
|
1862
|
+
* lines; a crowded bucket (minified code puts every function on one line)
|
|
1863
|
+
* switches to serialized keys. Consecutive records usually repeat the
|
|
1864
|
+
* previous value, which is checked first.
|
|
1865
|
+
*/
|
|
1866
|
+
class SlotTable {
|
|
1867
|
+
constructor() {
|
|
1868
|
+
this.values = [];
|
|
1869
|
+
this.buckets = new Map();
|
|
1870
|
+
this.last = undefined;
|
|
1871
|
+
this.lastKey = -1;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
slot(value) {
|
|
1875
|
+
if (this.last !== undefined && (value === this.last || sameJsonValue(this.last, value))) {
|
|
1876
|
+
this.last = value;
|
|
1877
|
+
return this.lastKey;
|
|
1878
|
+
}
|
|
1879
|
+
const key = this._find(value);
|
|
1880
|
+
this.last = value;
|
|
1881
|
+
this.lastKey = key;
|
|
1882
|
+
return key;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
_find(value) {
|
|
1886
|
+
const values = this.values;
|
|
1887
|
+
const probe = `${value.name ?? value.macro}\0${value.startLine ?? value.line}\0${value.endLine}`;
|
|
1888
|
+
let bucket = this.buckets.get(probe);
|
|
1889
|
+
if (!bucket) this.buckets.set(probe, bucket = { keys: [], byText: null });
|
|
1890
|
+
if (bucket.byText) {
|
|
1891
|
+
const text = JSON.stringify(value);
|
|
1892
|
+
let key = bucket.byText.get(text);
|
|
1893
|
+
if (key === undefined) {
|
|
1894
|
+
key = values.length;
|
|
1895
|
+
values.push(value);
|
|
1896
|
+
bucket.byText.set(text, key);
|
|
1897
|
+
}
|
|
1898
|
+
return key;
|
|
1899
|
+
}
|
|
1900
|
+
for (const key of bucket.keys) {
|
|
1901
|
+
if (sameJsonValue(values[key], value)) return key;
|
|
1902
|
+
}
|
|
1903
|
+
const key = values.length;
|
|
1904
|
+
values.push(value);
|
|
1905
|
+
bucket.keys.push(key);
|
|
1906
|
+
if (bucket.keys.length >= 32) {
|
|
1907
|
+
bucket.byText = new Map(bucket.keys.map(k => [JSON.stringify(values[k]), k]));
|
|
1908
|
+
}
|
|
1909
|
+
return key;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** Whether two JSON values serialize identically (key order included). */
|
|
1914
|
+
function sameJsonValue(a, b) {
|
|
1915
|
+
if (a === b) return true;
|
|
1916
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
|
|
1917
|
+
if (Array.isArray(a)) {
|
|
1918
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
1919
|
+
for (let i = 0; i < a.length; i++) {
|
|
1920
|
+
if (!sameJsonValue(a[i], b[i])) return false;
|
|
1921
|
+
}
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
if (Array.isArray(b)) return false;
|
|
1925
|
+
const keysA = Object.keys(a);
|
|
1926
|
+
const keysB = Object.keys(b);
|
|
1927
|
+
if (keysA.length !== keysB.length) return false;
|
|
1928
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
1929
|
+
const key = keysA[i];
|
|
1930
|
+
if (key !== keysB[i] || !sameJsonValue(a[key], b[key])) return false;
|
|
1931
|
+
}
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** In-memory form of a persisted calls-cache entry (see serializeCallsEntry). */
|
|
1936
|
+
function decodeCallsEntry(entry) {
|
|
1937
|
+
let tabled = null;
|
|
1938
|
+
for (const [field, key] of CALL_OBJECT_TABLES) {
|
|
1939
|
+
if (!entry[key]) continue;
|
|
1940
|
+
(tabled ||= []).push(field, entry[key]);
|
|
1941
|
+
delete entry[key];
|
|
1942
|
+
}
|
|
1943
|
+
if (!tabled) return entry;
|
|
1944
|
+
for (const call of entry.calls || []) {
|
|
1945
|
+
for (let i = 0; i < tabled.length; i += 2) {
|
|
1946
|
+
const at = call[tabled[i]];
|
|
1947
|
+
if (typeof at === 'number') call[tabled[i]] = tabled[i + 1][at];
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return entry;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1463
1953
|
/**
|
|
1464
1954
|
* Load a single calls shard by hash.
|
|
1465
1955
|
* @param {object} index - ProjectIndex instance
|
|
@@ -1483,7 +1973,7 @@ function _loadCallsShard(index, hash) {
|
|
|
1483
1973
|
// parse of current disk content (or an earlier load) — never
|
|
1484
1974
|
// clobber it with persisted shard data (fix #227).
|
|
1485
1975
|
if (!index.callsCache.has(absPath)) {
|
|
1486
|
-
index.callsCache.set(absPath, entry);
|
|
1976
|
+
index.callsCache.set(absPath, decodeCallsEntry(entry));
|
|
1487
1977
|
}
|
|
1488
1978
|
}
|
|
1489
1979
|
} catch (e) {
|
|
@@ -1617,8 +2107,9 @@ function buildWithLock(index, buildOpts = {}, options = {}) {
|
|
|
1617
2107
|
module.exports = {
|
|
1618
2108
|
buildWithLock,
|
|
1619
2109
|
saveCache, saveUsageCache, loadCache, loadCallsCache, isCacheStale, ensureCallsCacheLoaded,
|
|
2110
|
+
ensureCallsShardForFile,
|
|
1620
2111
|
getUserCacheRoot, getProjectCacheDir, getProjectCachePath,
|
|
1621
2112
|
getLegacyProjectCacheDir, migrateLegacyProjectCache, clearProjectCache,
|
|
1622
2113
|
clearAllCaches, pruneUserCache,
|
|
1623
|
-
_computeReachabilityFingerprint, CACHE_FORMAT_VERSION,
|
|
2114
|
+
_computeReachabilityFingerprint, CACHE_FORMAT_VERSION, discoveryFingerprint,
|
|
1624
2115
|
};
|