Repository navigation
Conversation
Undocumented helper functions widen to `any`, silencing hover/completion for every caller downstream. Adds an opt-in, heuristic usage-based inference engine to the b2c-script-types tsserver plugin that infers plausible types from call-site arguments and return statements, chasing through undocumented call chains, and surfaces the result as a labeled hover note and synthesized member completions. Off by default via the new b2c-dx.features.scriptTypesInferUsage setting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
- Handle the common export patterns that previously produced no inference:
bare `module.exports = function(){}`, destructured `const {x} = require(...)`
(including renamed bindings), and ES6 method-shorthand exports. Previously
only `exports.foo = function(){}` consumed via property access worked.
- Restore the `maximumLength` parameter on the getQuickInfoAtPosition
override, which was being silently dropped for all users regardless of
the inferUsage setting.
- Gate the new hover/completion overrides on the main `enabled` flag and
`isCartridgeFile()`, matching every other feature in this plugin, and fix
the VS Code extension side so `inferUsage` can't stay active once the
parent scriptTypes feature is disabled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
- Bound inferenceCache growth: clear the whole cache on project version change instead of leaving stale per-key entries around indefinitely. - Add request-scoped, depth-safe memoization plus a hard reference-count budget per inference request, so a widely-referenced helper can't make a single hover/completion synchronously fan out unboundedly. - Skip inference for parameters/returns that already have an explicit type annotation (TS syntax or JSDoc `@param`/`@returns`), even if that annotation is literally `any` — never override a type the developer or JSDoc already resolved on purpose. - Preserve every other CompletionInfo field (isIncomplete, optionalReplacementSpan, metadata, etc.) from the original completion result when merging in synthesized entries. - Correct a misleading comment claiming plugin state is shared across projects in a multi-root workspace — verified against the tsserver source that the plugin factory is invoked fresh per project, so this was never actually a cross-project leak. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
- Convert usage-inference.ts's function-level comments to JSDoc /** */ blocks with @param/@returns where the SDK convention already does this for every top-level function (exported or not); index.ts's own internal closures already use plain // comments consistently, so left as-is. - Match the VS Code setting description to the established "(Preview) ... In development — off by default." wording used by other not-yet-stable features, instead of an ad hoc "Experimental:" prefix. - Match doc phrasing in ide-integration.md to the sibling scriptTypes setting's plain-backtick + "(default: `false`)" convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
Correctness (empirically verified with repro scripts): - Check ctx.memo before the MAX_INFERENCE_DEPTH cap in inferParameterType/ inferReturnType, so an already-computed result isn't discarded just because the current call path happens to run over the depth budget. - Add PropertyAccessExpression handling to resolveExpressionTypes so `return x.prop` on an undocumented parameter chases the base's inferred type instead of giving up. - Use checker.getApparentType() before getPropertiesOfType() so inferred primitive types (string/number/boolean) get their wrapper-object member completions. - Widen literal call-site argument types to their general type, so hover shows `string` instead of a union of every literal ever passed. - Add a ctx.visiting cycle guard to inferParameterType (inferReturnType already had one), preventing re-entrant recomputation for self-forwarding helpers. - Cap how much of the shared reference budget a single collectCallSites call can spend, so one widely-referenced helper can't starve sibling branches processed later in the same request. Tooling: - Fix test/test:agent/test:unit/test:watch to explicitly rebuild first — pnpm's pretest hook only fires for the literal `test` script, so the other three were silently running against stale or missing compiled output. - Fix the shared test fixture host to include the default lib file (a LanguageService, unlike a Program, never adds it automatically), needed to test apparent-type-dependent behavior at all. Coverage: added regression tests for every fix above, plus previously- untested fixes (maximumLength forwarding, cache invalidation on project version change) — 33 tests total, up from 25. Conventions: clarified in the changeset why @salesforce/b2c-cli is listed despite no direct file changes, and matched log message phrasing / VS Code setting punctuation to this file's established style. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
… real dw.* type test coverage Completions silently fell back to plain global suggestions whenever the inferred type was nullable (e.g. ProductMgr.getProduct(): Product | null), since getPropertiesOfType on a union only returns members common to every constituent and null contributes none — strip the nullable part first. Also teach resolveExpressionTypes to chase a return expression that is a multi-hop method chain on the function's own untyped parameter (e.g. `return product.getPriceModel().getPrice();`), so an undocumented helper's own return type resolves instead of stopping at `any`. Add an end-to-end VS Code integration test exercising real hover/completion against a cartridge fixture, and a new test matrix exercising the inference engine against the actual bundled dw.catalog.Product/dw.order.Order/etc. types (not toy ambient stand-ins) across happy paths, deep nesting, and edge cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
- resolveExpressionTypes's method-chain branch (and the pre-existing property-access branch) didn't strip nullability before looking up a member, so a chain rooted in a real nullable SFCC getter (e.g. ProductMgr.getProduct(): Product | null) silently resolved to nothing. Extracted a shared getNonNullableApparentType()/getMemberOfType() helper used by both branches and typesToCompletionEntries, so the fix applies everywhere a candidate type's members are walked. - The new try/catch wrappers around the underlying language service calls unconditionally swallowed ts.OperationCanceledException (thrown whenever the host's CancellationToken fires, e.g. the user kept typing), turning ordinary request cancellation into a logged failure. Consolidated the four call sites into one guarded() helper that always rethrows cancellation. - The new chain-hop recursion had no cap distinct from MAX_INFERENCE_DEPTH and didn't dedupe per hop; added a dedicated MAX_CHAIN_HOPS bound and dedupeTypes() on the accumulated results. - Deduplicated the findFunctionDeclaration test helper into fixture-language-service.js and collapsed repetitive setup in usage-inference.real-types.test.js via a shared setupInference() helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018cSJNMGwtCicNegmdA83e6
Correctness: - Chase local-variable indirection: a chain split across intermediate vars (var priceModel = product.getPriceModel(); return priceModel.getPrice();) — the idiomatic SFCC style — now infers the same types as the inline expression, for return inference, hover on the variable, and member completions. Explicit @type annotations on a variable are respected (deliberate `any` is left alone), and mutually-referencing initializers are cycle-guarded. - Offer inferred completions when the receiver is a chained call (product.getPriceModel().|), not just a bare identifier. - Stop memoizing results whose computation hit a cycle guard — they are truncated by what happened to be on the call stack, and a later out-of-cycle query in the same request deserves the full answer. Error handling: - Only guard the plugin's own inference additions; exceptions from the underlying language-service calls propagate to tsserver exactly as they would without the plugin installed (cancellation was already rethrown; now genuine TS errors are no longer swallowed either). Packaging/infra: - Keep the license header in the emitted plugin/usage-inference.js (detach it from the type-only import that tsc elides). - Distinguish method vs property completion kinds; document sortText. - failZero on the infer-usage vscode-test label so a run that discovers zero tests fails instead of passing vacuously. - Trigger extension CI on packages/b2c-script-types/** changes and run CI for PRs targeting feature/** branches. - Add c8 coverage to the package test script; dedupe test:unit; ignore package-level coverage/ output. - Correct the reference-budget comment: it caps result processing and fan-out, not the cost of a single project-wide reference search. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3fJk97pe2QcEeMKFJqQv6
… inference
Studying real SFRA helper modules (app_storefront_base scripts/helpers,
scripts/cart) surfaced patterns the E2E suite never exercised: cross-file
call sites reached through */ and ~/ cartridge requires, the canonical
`module.exports = {name: name}` alias-map export, chain hops parked in
intermediate variables, and deep property chains with a nullable middle
step (availabilityModel.inventoryRecord: ProductInventoryRecord | null).
Engine fix found by this exercise: a reference search on a function
exported through an alias map dead-ends at the map's initializer, so the
SFRA-canonical export shape inferred nothing cross-file.
resolveIndirectReferenceTarget now hops from a property-assignment
initializer to the property name and searches on from there.
E2E fixture is now a realistic mini-cartridge: an undocumented
productHelpers module whose only call sites live in cartService.js via a
~/ require, plus a jsconfig.json so all cartridge files share one
configured project (the setup `b2c setup ide vscode-types` recommends) —
without it each open file gets its own inferred project and cross-file
reference search has nothing to look at.
New E2E tests (5): cross-file param inference through the plugin's own
module resolution, intermediate-variable hover, chained-receiver
completions, nullable deep-property-chain hover and completions. All
completion assertions now require at least one member that appears
nowhere in any fixture document, so VS Code's word-based suggestions
cannot satisfy them vacuously — the pre-existing test asserted only
members that literally occur in the fixture text.
Matching unit tests: alias-map export cross-file (mechanics) and the
nullable deep chain against the real dw.* types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3fJk97pe2QcEeMKFJqQv6
…s end-to-end SFRA plugin cartridges extend base modules through module.superModule — the runtime handle to the same-path module in the next cartridge down the cartridge path. TypeScript knows nothing about it, so `var base = module.superModule;` and everything derived from base was opaque `any`. Engine: resolveExpressionTypes gets a module.superModule leaf. The plugin supplies a resolver mapping a cartridge file to the same-subpath file in the next cartridge down (probing existence through the language-service host, not ts.sys); the super module's export type is read off its `module.exports = X` assignment via resolveExpressionTypes, so a pass-through overlay (`module.exports = base`) recurses another cartridge down naturally, with a visiting-guard against overlay cycles. When a member found this way is an undocumented function (declared return type `any`), inference recurses into its actual declaration instead of surfacing `any` — and `any`-typed members are never offered as candidates anywhere. Fixture host fix surfaced by these tests: directoryExists forwarded to the real filesystem, so TS's directoryProbablyExists pre-check silently failed nested relative requires between in-memory fixture files — directories implied by the file map now exist. Tests: five engine-level unit tests (export-type resolution, recursion into undocumented base helpers, no-lower-cartridge, no-resolver, and a three-cartridge pass-through chain), a real-dw-types composition test (superModule + alias map + var chains -> Money), a plugin-level test of the cartridge-path resolver wiring, and three VS Code E2E tests against a new app_custom_cartridge overlay fixture (ordered above test_cartridge via dw.json's cartridges field): hover through the full composition, base-module member completions, and Money member completions — E2E completion assertions filter out word-based (Text) suggestions so only typed entries can satisfy them. Docs: ide-integration guide documents superModule support and the one-project requirement (jsconfig) for cross-file inference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3fJk97pe2QcEeMKFJqQv6
…est server.append
Four more real-SFRA capabilities for usage-based inference:
Callback parameters: a function expression in argument position has no
name to run a reference search on, so its first parameter is now typed
from the element type of a collection-like sibling argument (anything
with iterator()/next() — dw.util.Collection and friends), covering the
`collections.forEach(coll, function (item) {...})` idiom. The collection
argument is resolved through the engine, so it works even when the
collection itself only exists via inference of the enclosing helper's
parameter. reduce-style callees are skipped (accumulator first), and
only the first parameter is mapped.
Manual iterator loops needed no engine change — iter/next() chains
already resolve through the chain machinery — but are now pinned down by
real-dw-types unit tests and an E2E test.
Multi-cartridge superModule stacks: the checker's merged module.exports
type is used when concrete (it carries `module.exports.name = fn`
augmentations), pass-through re-exports additionally recurse another
cartridge down, and members that exist ONLY as augmentations on a
pass-through level — invisible to any candidate type — are resolved by
a dedicated member walk down the cartridge chain (and listed in
completions). superModule-derived expressions also bypass the direct
checker-type short-circuit and the hover/completion any-gates: TS's own
type for them is garbage either way (any or an opaque circular typeof).
server.append middleware params turn out to need no inference: with a
modules cartridge present the plugin injects its SFRA ambient
declarations and TypeScript types (req, res, next) contextually from the
typed append() signature. Tests pin down both halves: the params ARE
typed, and inference does NOT decorate them.
Tests: 12 new unit tests (callback heuristics and guards, three-level
overlay stack with mid-level augmentation, plugin-level server.append
and overlay-stack wiring, real-dw-types callback/iterator coverage) and
7 new E2E tests against extended fixtures (modules + plugin_promo
cartridges, an SFRA-style collections util, variantHelpers, and a
controller), all completion assertions filtered to typed entries so
word-based suggestions can't satisfy them. Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3fJk97pe2QcEeMKFJqQv6
The engine runs synchronously inside tsserver on every hover/completion keystroke, and its dominant cost is getReferencesAtPosition (a project-wide scan per call). These tests pin down worst-case cost with deterministic search *counters* rather than wall-clock timings, so they hold on slow CI runners and point at exactly which cap regressed: - widely-referenced helper (300 call sites): <= 4 searches, per-call budget engages, correctness survives the cap - overlong method chain (60 hops): chain cap fires before any search - no-parameter helper chain: delegated to TS's native inference, 0 searches - 12-level parameter-forwarding chain: depth cap keeps cost flat in both directions (0 searches for returns, <= 6 from the deep end) - 20-branch fan-out through a shared sub-helper: request memo collapses it to <= 4 searches - repeated identical hover at the same project version: 0 new searches (served from the position cache) - mutual recursion with heavy call-site fan-in: terminates, budget never goes negative A generous wall-clock ceiling (5s) rides along on each scenario purely as a tripwire for catastrophic regressions (lost cap, infinite loop); the counters are the real baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3fJk97pe2QcEeMKFJqQv6
Performance audit of the usage-inference engine at SFRA scale (~1,900 cartridge JS files, 300-call-site hot helper, 3-level overlay stack) found two engine-level gaps, both measured with the real plugin driving a real LanguageService: 1. The number of getReferencesAtPosition SEARCHES per request was only indirectly bounded. MAX_REFERENCES_PER_REQUEST counts search *results*, but each search is a full project scan even when it returns almost nothing: a helper whose call sites feed it results of many DISTINCT exported sub-helpers (each searched once, each contributing 2-3 results) drained the result budget at ~2-3 per scan — 76 scans and 114ms p50 (141ms p95) for a single hover, measured before the fix. A new MAX_SEARCHES_PER_REQUEST budget (12; the costliest legitimate baseline scenario needs 6) bounds the scans directly: same fixture now runs 12 searches in ~42ms p50, still inferring the right type from the in-budget call sites. 2. Two implicit-any parameters of the same function each re-ran the identical reference searches within one request (the request memo keys parameter/function nodes, not searches): 4 scans instead of 2, measured. collectCallSites results are now memoized per name node for the request; reuse is sound because budgets only decrease within a request, so a memoized result is never less complete than a re-run would be. Also: getNodeAtPosition now stops scanning a sibling list once past the target position (siblings are ordered and non-overlapping). Every reference hit walks the AST from its file's root, so in a generated file whose array literal has thousands of elements the old full-list walk cost ~25ms per request on top of the search itself. Three new counter-based baseline scenarios pin these bounds: distinctSubHelperTree (<= 12 searches, and asserts the budget actually engages), multiParamHelper (<= 2), hugeGeneratedFile (<= 2 searches, <= 50 hits processed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFvM84JTUzSx4oaXjMabHb
Memory and churn findings from the same performance audit, both in the plugin-side cache wiring: 1. The per-position inference cache stored checker Type[] objects. A Type pins its checker and, through it, the whole Program it came from — so after an edit, the cache kept the entire previous program graph alive until the next inference-eligible request cleared it (indefinitely, if the user stopped hovering). The cache now stores finished display products instead — the hover note string and the synthesized CompletionEntry array — which retain nothing. "Inference found nothing" is now a cached answer too (it costs the same searches as a hit), and a 512-entry cap bounds a long no-edit session. 2. Invalidation now compares Program instances instead of the project version string. TS constructs a new Program for any semantic change and reuses the instance otherwise, while tsserver's project version also bumps on events that produce no new program — each such bump discarded a perfectly valid cache and re-ran full inference (measured ~13ms per hover on an SFRA-sized project, ~40ms including the request's incremental program update). New baseline scenario versionBumpSameProgram pins the second fix: a version bump with an unchanged program must answer from the cache with 0 new searches. Measured after both commits (SFRA-scale workspace, real edits between requests): hover p50 41ms / p95 80ms, completion p50 32ms / p95 49ms, superModule hover p50 20ms — all inside the 100ms hover / 200ms completion targets; warm-cache repeats ~1ms. Cancellation interrupts mid-request within ~0.3-3ms of the token firing (rethrown, checked inside every reference search). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFvM84JTUzSx4oaXjMabHb
Follow-up fix for the one audit finding previously left as inspection-only, now measured and confirmed real: candidate types propagate up through every recursion level (parameter -> return -> forwarding helper), and dedupeTypes re-stringified the same Type objects at each level — 192 typeToString calls for 48 unique candidate types, 13ms of a 34ms request, when 50 call sites pass large distinct object literals through a two-hop forwarding chain. dedupeTypes now goes through a request-scoped typeToString memo on the inference context (sound: rendering a type is pure for a given checker, and the context never outlives its checker). describeTypes separately stringified every candidate twice — once inside dedupeTypes, once to render — and now dedupes by display string in the same pass that renders it. Same fixture after the fix: 48 stringifications (exactly one per unique type), request wall time 34ms -> 19.5ms. New baseline scenario nestedForwardingStringifications pins the bound with a deterministic counter on the public checker.typeToString: 30 unique candidates through a three-level chain must stay <= 32 calls (unfixed behavior: one call per candidate per level, 120). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFvM84JTUzSx4oaXjMabHb
The tsserver plugin's require() resolvers join attacker-controlled import
specifiers (and a cartridge package.json `main`) onto a root directory
without asserting the result stays inside that root. A malicious repository
could therefore make a cartridge file resolve `require()` to a file OUTSIDE
the workspace and pull it into the TS program:
- resolveDwModule: `dw/../../../<abs>` escapes the bundled types dir
- resolveCartridgeModule: `~/..`, `*/..`, `<name>/..` escape the cartridge
- resolveModulesCartridge: `seg/..` and a package.json `main` of `../../..`
escape the modules cartridge
- an in-cartridge symlink pointing outside the root escapes too
Each resolved out-of-root file is returned as a resolvedModule, so TS reads
and parses it — an information-disclosure primitive (go-to-definition,
completion on the imported binding) driven purely by opening a cloned repo.
Add a shared canonicalize-and-contain guard (realpath to defeat symlinks,
then `..`/`.` collapse and case/slash fold) applied at every resolver's
trust boundary, plus resolveSuperModulePath for defense in depth. A
legitimately symlinked cartridge root still resolves because both sides are
realpath'd consistently.
Adds test/index.security.test.js: one regression test per traversal shape
(dw, ~/, */, <name>/, modules bare + package.json main, symlink), each
failing before this change, alongside guards that legitimate requires still
resolve. Tests drive the real create() proxy resolver against on-disk
fixtures, not mocks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
dw.json and a cartridge's package.json are attacker-controlled in a cloned repo and were parsed synchronously on tsserver's thread with no size bound — a multi-hundred-megabyte file is a denial-of-service vector (memory + parse time). Route both reads through a single size-capped reader (1 MiB ceiling; real files are a few KB) that never throws and yields `undefined` for a missing, oversized, or malformed file, which callers already treat as absent. Also guard the BASE_CARTRIDGE_RANK lookup in orderCartridges with a hasOwnProperty check so a cartridge directory named `__proto__`/`constructor` can't read an inherited prototype value and corrupt the sort comparator. Adds a regression test: a valid package.json with a valid in-root `main` but padded past the ceiling must be refused, not parsed (fails before the cap). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
The feature forwards workspace-derived cartridge names and filesystem paths
to the tsserver plugin, which resolves require() across the whole project
and (with inferUsage) reads sibling files to synthesize hover/completion —
work that should never act on an unvetted, freshly-cloned repository. It was
only implicitly trust-gated (an extension with a main entry and no declared
Workspace Trust support is disabled in Restricted Mode), which is fragile.
Make the posture explicit and robust:
- declare capabilities.untrustedWorkspaces.supported = false so VS Code
formally withholds the extension until the workspace is trusted;
- additionally gate the script-types push() on vscode.workspace.isTrusted
as defense in depth (forwards a disabled config while untrusted), and
re-push on onDidGrantWorkspaceTrust so the feature turns on without a
reload once the user vouches for the workspace.
This also neutralizes a workspace .vscode/settings.json shipped by a repo
force-enabling the preview inferUsage flag: such settings only take effect
in a trusted workspace, which is now a prerequisite for the feature at all.
Adds a changeset covering the full security-hardening pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
The inference engine had grown to a single ~1,200-line src/usage-inference.ts (and a ~1,070-line compiled twin), which is hard to navigate. Split it by responsibility into small, single-purpose modules under src/inference/ behind a re-exporting barrel, keeping the public API (and the `../plugin/usage-inference` import path the tests use) unchanged: constants - the tunable limits that bound a request context - the per-request scratchpad (program, budgets, memo, guards) ast-helpers - pure AST navigation (find node, enclosing access, returns) call-sites - find where a function is called across the project type-helpers - Type utilities + hover text / completion entry synthesis super-module - module.superModule detection and export scanning (leaf ops) core - the recursive engine that ties the leaf modules together Dependencies flow one way (core -> leaves; leaves never import core), so there are no import cycles. The recursive functions that must call each other stay together in core.ts; only genuinely independent helpers were extracted. Pure code move — no behavior change. Largest module is now 503 source / 476 compiled lines. All 104 unit tests pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
index.ts had grown to ~910 lines. Move the pieces that don't touch the
plugin's mutable per-project state into two focused modules under
src/resolver/, leaving index.ts as just the plugin lifecycle (config,
module-resolution wiring, hover/completion overrides):
resolver/constants - shared cartridge types + lookup tables
(ambient module names, candidate extensions,
base-cartridge ranks, discovery ignore list,
JSON size ceiling)
resolver/cartridge-discovery - the size-capped JSON reader, .project
discovery walk, dw.json cartridge-order
parsing, ordering, and declare-module range
scanning — all pure/standalone functions
taking `ts` (+ a fileExists probe) as plain
arguments
The bundled-types path constants (TYPES_DIR/GLOBAL_DTS/SFRA_SERVER_DTS) stay
in index.ts because they resolve against `__dirname`, which must point at the
plugin's own output location. Pure code move — no behavior change. index.ts
is now 724 source / 659 compiled lines. All 104 unit tests and the 17 VS Code
E2E tests pass unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
fallow/ESLint flagged these two as the codebase's complexity hotspots.
Decompose them without changing behavior:
- resolveExpressionTypes is now a small dispatcher that routes an `any`
expression to one of three focused resolvers by kind — resolveCallResultTypes
(call / method-chain), resolvePropertyTypes (property access), and
resolveIdentifierTypes (parameter / local variable). Each returns [] where
the original fell through, so the dispatch is equivalent.
Cyclomatic 29 -> 7, cognitive 61 -> 6.
- collectCallSites now delegates the inner per-reference processing loop to
collectCallsFromName, which classifies each reference hit into a call site
or a next-hop name and threads the remaining local budget back via its
return value. Cyclomatic 19 -> 9, cognitive 37 -> 12.
The worst function in the package drops from cognitive 61 to 14. Pure
mechanical extraction — all 104 unit tests and the 17 VS Code E2E tests pass
unchanged, and core.ts stays under 800 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
The b2c-script-types package pulled c8 into its own importer purely to run a coverage-wrapped `test` script, which was the only net-new entry in pnpm-lock.yaml on this branch and tripped the third-party-dependency review gate. The package's tests run fine under plain `node --test` (test:agent, used by CI, never invoked c8), so drop the c8 devDependency and the `c8` wrapper from the `test` script. With c8 gone from the only package that added it, the lockfile is byte-for-byte identical to main again (the remaining diff was a cosmetic importer reorder), so `pnpm install --frozen-lockfile` passes and there are no net-new third-party dependencies to review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GzksxTHf9NL8qyj59iRfr
When call-site inference finds nothing (a helper only reachable indirectly, or genuinely dead code), match a parameter or local variable's own member/method usage against the vendored dw.* ambient classes to recover its type. Also covers the manual-indexing loop idiom (var item = items[i]) where the element type can only come from the loop variable's own downstream usage. Fixes found while dogfooding this against a real cartridge: - Hover on a chained access's member name (not just the receiver) resolved nothing even when the receiver inferred fine. - The ambient class index was cached per-Program instead of per LanguageService, so it rebuilt on nearly every keystroke while editing a cartridge file, making completions unreliably slow on large real projects. - Hover now borrows the real declaration's display header, doc comment, and JSDoc tags instead of a bare "Inferred from usage: X" note. - A class's nested custom-attributes interface (ICustomAttributes.X) rendered with the same display name as the unrelated top-level class X. - A dangling, mid-edit member access (`shipment.` immediately followed by more code on later lines) could parse together with the next statement, poisoning usage-based matching with a phantom member and silently producing no completions for the position being typed. Adds unit coverage in b2c-script-types and end-to-end VS Code integration coverage (hover + completions) in b2c-vs-extension for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Debugging output left over from diagnosing the earlier flaky assertions; the failure-path assert messages already report the full completion label list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts security-critical cartridge module resolution and path-containment logic out of index.ts's closures into a testable resolver/module-resolution.ts module, deduplicates the legacy/modern module-resolution host hooks, hardens the JSON size-cap fallback, fixes a workspace-trust label bug in the VS Code extension, and cleans up docs/changeset/CI trigger inconsistencies found during review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cess Reported from real-world dogfooding (neuhaus-core's addressHelpers.js): removing JSDoc from getAddressBookAddressByForm(addressBook, form) lost hover/completion for addressBook entirely, since its only direct member access is addressBook.addresses — a single-member usage signature, below the existing two-member ambiguity threshold. That threshold exists to reject common member names (e.g. .custom) shared by many ambient classes, but .addresses is declared by exactly one: dw.customer.AddressBook. matchAmbientTypesByUsage now accepts a single-member signature when it's globally unambiguous, while still rejecting one that ties across multiple classes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the real-world case fixed in 63d67de: a no-call-site parameter resolved via the single-unique-member ambient-class match. Asserts the ambient-class index (every dw.* class's member-name set) is built once per LanguageService and a repeated hover at the same position adds zero further getPropertiesOfType calls, per the existing deterministic-counter convention in this file (wall-clock is only a catastrophic-regression tripwire). Measured directly: cold hover ~34ms (1470 getPropertiesOfType calls to build the index against this fixture's real dw.* program), warm hover 0.005ms (0 additional calls) — confirms the fallback stays cheap on repeated keystrokes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…k-by-reference inference
Corpus cases for `.call(this, ...)` model inheritance, `.apply(this,
arguments)` forwarding, contextual types from typed API calls (primitive
and class), `a || {}` initializers, reassigned-variable unions and named
element callbacks. The VS Code integration suite gains hovers for
`.call()` inheritance, a never-called parameter typed by
`ShippingMgr.applyShippingCost`, and an IntelliJ-style union hover.
Fixture files are now Prettier-clean; the deliberate mid-edit fixture is
listed in .prettierignore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
… changesets The IDE guide and package README now list the evidence inference weighs (call sites incl. new/.call/.apply and callbacks, returns and chains, typed Script API uses, the helper's own body), the union-of-call-sites behaviour and names as a tiebreak only. Limitations drop ES6 classes and arrow exports (now supported) and add HookMgr.callHook and ISML pdict. The inference changeset now describes the final feature instead of the branch history and no longer quotes a project-specific custom attribute; the security hardening gets its own changeset. Regenerates the stale tooling index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…iables in ground truth The harness now says why each silent parameter stayed silent (no call sites, budget, dropped by policy, no evidence) and re-infers local variables whose Script API type only came from the blanked JSDoc, the end-to-end effect users see in hovers and completions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
- Collapse several instantiations of one generic class into the declared class before counting union members, so collections.forEach/map/find helpers called with Collection<A>, Collection<B>, ... show `Collection` instead of staying silent (arrays are never merged). - Never match TopLevel `Module`/`arguments` by usage, and stay silent when a JavaScript built-in (String, Array, ...) also has every used member, so `msg.replace()` is no longer an XML and `x.id` no longer a Module. - When filtering call-site candidates, ignore used members no candidate, ambient class or built-in declares (stale vendored types, expandos); ambient matching without call sites stays strict. - Render generic class instantiations as `Collection<Product>` instead of leaking `Product<any>` type arguments. - Ground-truth harness: split the silent-reason classifier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…ric pass-through and downcasts Usage inference now follows values that reach a parameter, return or variable through more than a direct call: - arrays built with push()/unshift()/x[i] = v, read back by index, by native array methods (filter, find, map callbacks typed from the lib's own declarations) or spread; arrays render as `Product[]` - element access on Script API collections without an index signature - untyped members read as the values they were built with: object literal members, shorthand members, this.member in prototype methods, and members written on untyped locals - generic Script API calls whose result is an argument or a callback's return (Transaction.wrap(cb), identity<T>(x)), which JavaScript otherwise instantiates to any - a call-site argument class whose own members do not cover the body's usage narrows to its single fitting subclass (PaymentInstrument -> OrderPaymentInstrument), and stays the base class when several fit Callee resolution and the element-first callback test move into signatures.ts. The ground-truth harness now scores arrays element-wise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…d arguments; read this-stored and forwarded usage
Usage inference now finds the call sites of a function that travels as a
value before it is called, the way SFRA's search refinement models do:
- `module.exports = Name` and `exports.x = Name` lead to the require()
calls and member accesses of the module
- an unbound `return require('...')` makes the enclosing function a
factory, whose call results are followed like the function itself
- a function passed as an argument is followed into the receiving
parameter, where `new Model(...)` / `Model(...)` finally runs
Parameter usage also covers what IntelliJ reads:
- a constructor argument kept as `this.x = x` is used wherever the file
reads `this.x` (prototype methods included)
- a parameter passed unchanged to an undocumented project function
(`helper(x)`, `new Model(x)`, `Base.call(this, x)`) inherits that
parameter's members and contextual types, up to three functions away
Searches for a function-local name (a parameter or local variable) scan
one file and no longer spend the project-wide search budget; the
reference-hop limit goes from 2 to 4 to cover the refinement chain.
On the real-cartridge ground truth, parameter recall rises from 84.2% to
94.1% at 97.2% precision; at most 12 project-wide searches per request,
p95 latency 42.7 ms. Two undocumented `address` callbacks that used to
show OrderAddress are now silent: the array they iterate mostly holds
plain-object copies whose `states`/`country` members OrderAddress lacks.
The ground-truth harness reports project-wide and single-file searches
apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…ceholders as weak
A helper whose return depends on what one call passes it (SFRA's
collections.find / collections.first over basket.shipments) is now inferred
again for that call with its parameters bound to the call's arguments, when
its return across all callers says nothing or leaves a union. A bound argument
also narrows a declared any-instantiation such as a bare
{dw.util.Collection}, so the element type flows through iterator().next().
Results under bindings never enter the request memo.
{Object|null}, {?Object}, {Object=} and similar nullable or optional
placeholders are now weak like {Object}, so they no longer block inference.
superModule resolution moves from core into super-module behind a resolver
callback, keeping core within its size budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…pending depth A caller passing its own untyped parameter on as is (SFRA's addProductToCart -> getExistingProductLineItemInCart -> getExistingProductLineItemsInCart -> getMatchingProducts) is the same value, so its call sites are now searched at the same inference depth; an anonymous callback's parameter, read off the call it is passed to in the same body, costs no depth either. Such chains are bounded by the per-request search budget instead of the depth cap, and the forwarding-chain perf baseline records that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…d hook scripts
Hook scripts are never called by name: the platform dispatches
HookMgr.callHook(extensionPoint, functionName, ...args) to the scripts a
cartridge's hooks.json registers. Usage inference now reads those
registrations (package.json `hooks` -> hooks.json, parsed as JSON only,
paths contained to the cartridge) and follows callHook in both
directions: a hook export's parameters take the arguments of the
callHook calls that reach it, and a callHook call returns what the
matching exports return. Extension points built at runtime
('app.payment.processor.' + id, template literals) match by their
literal prefix. One project-wide search per request finds every
callHook call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…oks; cover them in VS Code The extension point of a HookMgr.callHook call is now also read through a local variable written once (`var hookName = 'app.payment.processor.' + id`). The IDE guide, package README and changeset describe the evidence sources added in this pass (member and array values, factory-returned models, call-specific helper results, generic Script API calls, hooks) and the remaining limitations. The VS Code integration workspace gains factory-model, collections.find and payment-hook hovers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…ce search Usage inference asked TypeScript's find-all-references for every name it followed. Its import tracker scales with the whole project, so on real cartridge stacks most of a hover's time went into reading files that cannot name the value at all. src/inference/reference-search.ts reads only the files that can: - a name scoped to one file (a local helper, a parameter, a variable) is searched in that file only, the same scoping TypeScript applies; - a module's exports are searched in the module itself and in the files whose require() paths load it; - anything else (plain-script globals) is still read project-wide, with a per-file name prefilter before the checker confirms each hit. Matches are confirmed by symbol identity, through aliases, destructured requires and shorthand properties. Results are cached per Program, the search honors the host's cancellation token, and each uncached search is published on a diagnostics_channel so tests and tooling can count them. On the real cartridge corpus, total inference time drops 29 % (p95 44.9 ms to 31.7 ms, max 157 ms to 95 ms) and parameter precision and recall both rise slightly (one previously silent parameter now resolves). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…rray} as a placeholder
collections.map(basket.shipments, function (shipment) {...}) said nothing:
SFRA documents map as `@returns {Array}`, which counted as a deliberate
annotation, and the callback it calls is a parameter with no declaration
of its own. Both are fixed:
- A bare `{Array}`, `{Object[]}` or `{Array.<*>}` says no more about its
elements than `{Object}` says about a value, so it no longer blocks
inference (a deliberate `{any[]}` still does).
- When a helper is inferred for one call, a call of a bound callback
parameter (`callback(item)`, `callback.call(scope, item)`) returns what
the function that call passes returns, with that function's parameters
bound to these arguments, also through wrappers that pass it on.
- A union across a helper's callers no longer outranks the answer for the
call at hand: that call is one of the callers, and the union may have
missed it when a budget ran out (SFRA's arrayHelper.find).
On the real cartridges getShippingModels now returns ShippingModel[], sort
comparators over mapped arrays see the mapped shape, and no previous
answer regressed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
An array filled with values of different classes (`[product, basket]`, or push() calls with each) was left silent. It now reads as an array of their union, decided by the same union policy as any other choice of types (at most three, else a shared superclass, else silent), and its elements flow into native array callbacks as either type. This replaces a wrong usage-only guess on real cartridges (a view-model array read as MediaFile) with the objects the code actually pushes. Unions inside arrays now render with the same class names as everywhere else (`(Product | Basket)[]`, not the checker's `Product<Product>`), and an array of booleans reads as `boolean[]`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
… calls it with
A callback passed to an undocumented helper used to get a type only when
the helper's name was on a fixed element-first list (`collections.forEach`,
`map`, `find`, ...), and only for its first parameter. The helper's own body
now says it: each `callback(item)` / `callback.call(scope, item)` call is
read, and its arguments are resolved with the helper's parameters bound to
the call at hand. `collections.reduce(lineItems, function (total, lineItem)
{...})` types `lineItem`, a forEach index is a number, and a project's own
helpers and wrappers that hand the callback on to another helper work the
same way. An accumulator the callback's result is fed back into stays
unknown, as with a native reduce. The name list remains as the fallback for
helpers whose source can't be read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
Walks looking for identifiers by name (reference searches, usage profiles, this-property and callback-use scans) now skip every subtree whose source text holds none of the names, found by one indexOf pass and a binary search per node. Subtrees holding a unicode escape are always read, so escaped identifiers are still found. Callee resolution skips the checker's overload resolution, which checks every argument a call passes, when the callee has only one signature, and editor hooks reuse the Program they already hold instead of asking the language service for it again. Same results on the real-cartridge ground truth; about 9% less total inference time, with a lower p50 and max. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
SFRA calls each middleware step with its Route as `this`, and steps
register route event listeners with `this.on('route:BeforeComplete',
function (req, res) {...})`. The Middleware type now declares that
`this`, so Route.on's listener signature types those parameters; on the
real cartridges 27 listener parameters go from `any` to Request/Response.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
… callback types
A named function passed to a callee no project parameter receives (an SFRA
middleware module handed to `server.get('Show', cache.applyDefaultCache)`, a
helper passed to a native `forEach`) had no call site to learn from. Such an
argument is now a handoff: the callee's declared callback type, rest
parameters included, types the function's parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…their own
Unit tests call SFRA middleware helpers with object literals that mimic the
real argument (`{httpHeaders: {get: ...}}` for a `Request`). Each one took a
slot of the three-type union, cluttering hovers (`Request | {...}`) and, with
enough mocks, silencing the parameter altogether. An object literal whose
properties a class candidate all has now stands in for that class instead:
on the real cartridges 6 more placeholder-documented parameters are typed and
11 unions read as the class, with no other result changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…lls back from The fit rule dropped every call-site candidate lacking a member the body reads, including members read defensively: `if (res.setStatusCode)`, `(res && res.statusCode) || res.errorCode`, `lineItemCtnr.orderNo || lineItemCtnr.UUID`. A member read only as a condition, a negation, a `typeof` operand, the left side of `&&` or a side of `||`/`??` is now optional, like one tested with `'m' in x`; one the body also relies on elsewhere stays required. On the real cartridges this keeps the SFRA `Response` handed to middleware that probes for non-SFRA members, with no change to the documented ground truth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…its calls go
An anonymous function a factory returns has no name to search for, so its
parameters stayed `any`: the common SFRA pattern `server.append('Confirm',
jsonResponse.responseHandler(fieldMap))`, with `responseHandler` returning
`function (req, res, next) {...}`, or a project calling the result
(`var listener = listenerFor('cart'); listener(basket)`). The call-site
search now starts from the factory in its factory role, so the function is
followed wherever the factory's calls take it, handoffs to declared APIs
included. Call-site results are memoized per searched name and role.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
… on every request tsserver asks the plugin for the project's file names on every language service request; the plugin normalized every path and probed the disk each time to decide whether to inject the ambient declarations. The answer is now kept until a file name or the configuration changes. Usage inference rebuilt a lowercased name table over every ambient class and copied the used member names once per class on each match. The table is now built once per Program and the member scan shared by both matchers. Ground truth results are unchanged; the engine's own time on the heaviest hovers drops by about 40%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
A declared function type such as SFRA's `NextFunction` is an anonymous
type literal with no properties, so the open-type check took it for an
empty `{}`. Hovers on contextually typed callback parameters ran
inference for nothing, and a function type passed down as call-site or
callback evidence was discarded as uninformative. Now a callable type
closes the gate and counts as evidence, so a middleware factory's
`next` shows `NextFunction`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…in the engine map Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
…is) members Two inference regressions from the evidence/policy restructure: - A `this` argument passed from inside a constructor function or class produced no evidence. The checker types it as the open `this` type, which the engine drops as a type parameter; it now reads as the instance that type stands for (SFRA queryString's `parameterToArray(this, ...)`). - Members a constructor defines with `Object.defineProperty(this, 'x', ...)` are missing from the checker's instance type, so the policy's member fit dropped such a model whenever the body used one of them (SFRA's Request passed to Route). Policy member checks now count those members; the scan is cached per declaration and skips nested functions. On the real-cartridge corpus, placeholder-JSDoc params typed go from 440 to 446 (all six verified against their call sites); documented-param ground truth, search counts and latency are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8VMhfY1J4xS85RwACb5hg
|
@taurgis I finally reviewed. I think this looks good. Only big thing that came up was the untrusted workspace change you made here and, after review, I think that's a good change overall. Only ask is to split the changesets to include that workspace hardening as a distinct change: something like Can you also shorten the main changeset to be appropriate for the release notes (little more feature description; can add to the docs more if you want to expand on the configuration for this feature): https://salesforcecommercecloud.github.io/b2c-developer-tooling/releases/ (and obviously merge main or rebase) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVfax6Ld8eB9oDGszpnWEa
Address review feedback: the VS Code extension's Restricted Mode (untrusted workspace) change gets its own minor changeset, the tsserver plugin hardening stays a patch, and the usage-inference changeset is cut down to a release-notes summary that points at the IDE integration guide. Document the workspace-trust requirement in the guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVfax6Ld8eB9oDGszpnWEa
Fold the SFRA `this` typing, the request-cost fix and the plugin hardening into one short patch changeset, and trim the usage-inference changeset further. The VS Code extension's Restricted Mode change keeps its own changeset, as requested in review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVfax6Ld8eB9oDGszpnWEa
Summary
Today, an undocumented SFCC helper function (no JSDoc) gets its parameters and return value widened to
anyby TypeScript, and thatanysilently poisons every downstream caller — hover and autocomplete just stop working for anything that touches it. This is extremely common in real SFRA cartridges.This PR adds an opt-in, heuristic fallback to
@salesforce/b2c-script-types: when the checker has already given up withany, the plugin looks at how the value is actually used elsewhere in the project — call-site arguments, return expressions, chained property/method access,module.superModuleoverlays — and infers a plausible type from that usage. It surfaces the result as hover text ("Inferred from usage: …") and synthesized member completions, without ever overriding a type TypeScript or JSDoc already resolved correctly.@salesforce/b2c-script-types, wired into the plugin'sgetQuickInfoAtPosition/getCompletionsAtPosition, behind a newinferUsageconfig flag (off by default).b2c-dx.features.scriptTypesInferUsage(off by default, labeled Preview/heuristic) plumbed through to the plugin.dw.*classes, callback/iterator element types, and multi-cartridgesuperModuleoverlays.Also included, hardening the feature before it ships:
require()path — including a cartridge'spackage.jsonmain— is canonicalized and contained, so a crafted import specifier or an in-repo symlink can no longer resolve to a file outside the bundled types directory or the cartridge roots.dw.json/package.jsonparsing is size-bounded to guard against oversized-file denial-of-service. The VS Code extension declarescapabilities.untrustedWorkspacesand refuses to forward cartridge paths or run inference until the workspace is trusted.Screenshots
Setting is off by default and clearly labeled as a preview/heuristic feature:
Hover surfaces the inferred type plus the real declaration's own docs, labeled so it's clear it's a heuristic guess, not a resolved type:
Completions for an otherwise-
anyvalue are populated from the inferred type's real members:Testing
@salesforce/b2c-script-types(node --test): the inference engine against both ambient stand-ins and the real bundleddw.*declarations (happy paths, deep chains, nullable types, unioned call-site types, budget/depth caps, cancellation), the tsserver plugin's proxy wiring (hover + completions), and a dedicated path-traversal/JSON-size security suite with one regression test per traversal shape.vscode-test) drives the real extension headless and asserts hover/completion against the realdw.catalog.Producttype across single-file, cross-file, chained, nullable, callback/iterator, and multi-cartridgesuperModulescenarios.Dependencies
3pl-approvedis set by a maintainerpnpm test)pnpm run format)