Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
73006c4
Infer types for undocumented helper functions in Script API IntelliSense
claude Jul 20, 2026
10e1699
Fix usage-inference gaps found in code audit
claude Jul 20, 2026
574e3df
Address remaining efficiency/correctness findings from code audit
claude Jul 20, 2026
a57bf61
Align new code with repo conventions for consistency
claude Jul 20, 2026
e37d116
Fix all findings from second code-review pass
claude Jul 20, 2026
6e3eb8c
Fix nullable-union completion bug and deep return-type chain gap; add…
claude Jul 20, 2026
fc335ec
Fix findings from code review of the nullable-union/deep-chain fix
claude Jul 20, 2026
b02a9f7
Fix audit findings in usage-based type inference
claude Jul 20, 2026
21b77cc
Harden E2E against real SFRA cartridge patterns; fix alias-map export…
claude Jul 20, 2026
142953a
Support module.superModule in usage inference; test cartridge overlay…
claude Jul 20, 2026
fac0428
Infer callback params, iterator elements, multi-cartridge overlays; t…
claude Jul 20, 2026
afd4b25
Add deterministic performance baselines for usage inference
claude Jul 20, 2026
0545121
Cap full-project scans per request; dedupe sibling-parameter searches
claude Jul 20, 2026
ccb568a
Cache inference display products, invalidate on Program identity
claude Jul 20, 2026
73c2702
Memoize typeToString per request; render hover text in one pass
claude Jul 20, 2026
894524b
Contain module resolvers against path traversal from cloned repos
claude Jul 20, 2026
fe8cb61
Bound untrusted JSON parsing and harden cartridge-rank lookup
claude Jul 20, 2026
a0a676f
Gate Script API IntelliSense behind Workspace Trust
claude Jul 20, 2026
1509d3e
Refactor usage-inference into focused modules under 800 lines
claude Jul 20, 2026
a3766ae
Extract cartridge discovery and constants out of index.ts
claude Jul 20, 2026
16f46bb
Reduce complexity of resolveExpressionTypes and collectCallSites
claude Jul 20, 2026
54e844f
Merge pull request #2 from taurgis/claude/vscode-intellisense-subfunc…
taurgis Jul 20, 2026
41ea62d
Drop c8 from b2c-script-types; restore lockfile to match main
claude Jul 20, 2026
6278196
Add usage-based fallback inference for undocumented Script API values
taurgis Jul 21, 2026
5d93175
Remove leftover diagnostic console.log from infer-usage integration test
taurgis Jul 21, 2026
ffe497b
Address PR audit findings in Script API IntelliSense plugin
taurgis Jul 21, 2026
63d67de
Recover usage-inferred types from a single, globally-unique member ac…
taurgis Jul 21, 2026
be920c9
Add perf baseline for the addressBook.addresses usage-match scenario
taurgis Jul 21, 2026
53078d0
Recognize 'member' in obj as usage evidence, from omoda-core survey
taurgis Jul 21, 2026
9534289
Recognize `new Helper(x)` as a call site, from mul-core survey
taurgis Jul 21, 2026
50643df
Consolidate PR changesets into one
taurgis Jul 21, 2026
3460635
Fold consolidated changeset content into script-types-infer-usage.md
taurgis Jul 21, 2026
6357d6c
Prefer identifier-name match over size when ambient classes tie; shar…
taurgis Jul 21, 2026
9cc724b
Share the test DocumentRegistry in index.test.js too
taurgis Jul 21, 2026
945d026
Share the test DocumentRegistry in the remaining perf-test fixtures
taurgis Jul 21, 2026
4c3a874
Fix cartridge-relative resolution returning case-folded paths
taurgis Jul 21, 2026
385eda3
Harden usage inference: silence weak/conflicting guesses, expand SFRA…
taurgis Jul 21, 2026
bb53c03
Fix dogfood false positives for Store/address and Customer inference
taurgis Jul 21, 2026
83044b6
Expand storefront inference corpus and index generic Product ambient …
taurgis Jul 21, 2026
54e2ce9
Ignore placeholder SFRA JSDoc so usage inference can recover dw.* types
taurgis Jul 21, 2026
bec37e5
Align usage inference closer to IntelliJ SFCC storefront patterns.
taurgis Jul 21, 2026
d066c61
Add PascalCase naming aliases and VS Code regressions for inference.
taurgis Jul 21, 2026
d89a17d
Fix Prettier wrapping in infer-usage VS Code tests.
taurgis Jul 21, 2026
cab34f9
Fix @param {Object} gate and *LineItem naming false friends in usage …
taurgis Jul 21, 2026
9b871e8
Merge branch 'main' into feature/script-types-usage-inference
taurgis Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/script-types-infer-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@salesforce/b2c-cli': minor
'b2c-vs-extension': minor
---

Script API IntelliSense can now infer types for undocumented helper functions from how they're actually called elsewhere in your project, instead of silently falling back to `any` and losing hover/completion for everything downstream. This is off by default — enable it with the `b2c-dx.features.scriptTypesInferUsage` VS Code setting (or `inferUsage: true` in the plugin config for other LSP hosts). Inferred results are clearly labeled ("Inferred from usage") since they're heuristic. `@salesforce/b2c-cli` picks this up too since `b2c setup ide vscode-types`/`tsserver-plugin` bundle the same plugin.

Beyond call-site and return-expression inference, the engine also recognizes:
- A parameter or local variable's own member/method accesses (e.g. `shipment.custom`, `shipment.productLineItems`) matched against the Script API's ambient classes, when no call site or usable initializer can resolve its type at all — recovering hover/completions for helpers only reached indirectly (e.g. dispatched from a Controller route), and for collection items pulled out with a manual indexing loop (`var item = items[i]`) instead of `collections.forEach`.
- A single accessed member when it uniquely identifies one Script API class (e.g. `addressBook.addresses` only matches `dw.customer.AddressBook`), instead of only ever guessing from two or more accessed members. A lone member name shared by several classes (e.g. the common `.custom` attribute pattern) is still correctly left unresolved.
- A `'member' in obj` existence check (e.g. `'Subsoort' in apiProduct.custom`) as evidence of that member, not just a direct `obj.member` read — a very common SFCC idiom for guarding an optional custom attribute before reading it.
- `new Helper(x)` constructor calls as a call site, not just plain `helper(x)` calls — SFRA's other very common way to invoke an undocumented "class" model (e.g. `new ProductLineItem(...)`, `new StoreModel(...)`).

When a member signature still matches more than one Script API class, a parameter or variable conventionally named after the class it holds (`profile` for `dw.customer.Profile`, `shipment` for `dw.order.Shipment`) is now preferred over the previous "fewest total members" tiebreak alone — which could otherwise pick a small, unrelated class purely because it exposed less surface area than the large, correct one (e.g. `dw.customer.ProductListRegistrant` over `dw.customer.Profile` for a variable literally named `profile`, since both happen to share a common `email`/`firstName`/`lastName`/`custom` field subset).

Also fixes several bugs uncovered while dogfooding this against real projects:
- Hover showed nothing when hovering the member name itself in a chained access (e.g. `productLineItems` in `shipment.productLineItems`) even though hovering the receiver worked.
- Completions were slow/unreliable on large real projects because an internal cache was invalidated on every keystroke instead of once per project session.
- Hover now shows the real declaration's own type name, documentation, and JSDoc tags (not just a bare "Inferred from usage: X" note).
- A class's nested custom-attributes interface (`ICustomAttributes.Shipment`) rendered with the same display name as the unrelated top-level class it's attached to.
- A dangling, mid-edit member access (`shipment.` immediately followed by more code on later lines — `.` never gets automatic semicolon insertion) could get parsed together with the next statement, poisoning usage-based matching with a phantom member name and silently producing no completions for the position being typed.
- The most common placeholder JSDoc of all, `@param {Object}`, silently got no inference in the editor: `checkJs` resolves capital-`Object` to the global `Object` interface (not `any` or the lowercase `object`), which the hover/completion entry gate treated as a real type and skipped — so the headline "undocumented SFRA helper" case produced nothing in a live project even though the engine handled it. Weak `{Object}` now opens the gate like the other placeholders.
- A parameter named after a *specific* `dw.order` line-item subclass (`bonusDiscountLineItem`, `productShippingLineItem`) was mis-resolved to `ProductLineItem` by the generic `LineItem` naming heuristic. These now resolve to their own class when the body's usage fits it, and stay silent otherwise, rather than guessing the wrong sibling.

Also tightens Preview trust: conflicting call-site argument types stay silent instead of unioning a noisy hover; ambient matches rank by member distinctiveness (so ubiquitous `.custom` / `.UUID` don't dominate); and element-first callbacks cover `collections.map` / `filter` / `every` / `some` / `find` / `first` (not only `forEach`). Cartridge `~/` / `*/` require resolution now consults the language-service host filesystem (not only `ts.sys`), so virtualized hosts and tests resolve the same way as a real project. Call-site types that don't expose every member the parameter body actually uses are dropped (so a duck-typed Store model passed into an address helper can't win the hover); a conventionally named parameter with a single strong member (`customer` + `.profile`) is trusted even when that member is shared by another ambient class; SFRA aliases (`lineItem` / `pli` → `ProductLineItem`, `priceModel` → `ProductPriceModel`, …) and CamelCase suffixes (`resettingCustomer` → `Customer`, `apiProduct` → `Product`, `currentBasket` → `Basket`) get the same short-circuit; generic Script API classes like `Product<T>` are included in ambient matching (shown as `Product<any>`); ternary returns (stock `collections.first`) and `instanceof` class checks feed inference the same way JetBrains' JS evaluator does; and placeholder SFRA JSDoc (`@param {Object}` / `{obj}` / `{*}` / `{}`) no longer blocks usage inference — only deliberate `{any}` and real `dw.*` annotations stay authoritative, matching how IntelliJ helps when authors write real types while still recovering the common undocumented storefront helpers.

Includes security hardening against malicious repositories: the tsserver plugin now canonicalizes and contains every resolved `require()` path (including a cartridge `package.json` `main`) so a crafted import specifier or symlink in a cloned repo can no longer resolve to a file outside the bundled types directory or the cartridge roots, bounds the size of `dw.json`/`package.json` it parses, and the VS Code extension now declares that Script API IntelliSense requires a trusted workspace (`capabilities.untrustedWorkspaces`) and refuses to forward cartridge paths or run usage inference until the workspace is trusted.
2 changes: 2 additions & 0 deletions .github/workflows/ci-vs-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- develop
paths:
- 'packages/b2c-vs-extension/**'
- 'packages/b2c-script-types/**'
- 'packages/b2c-tooling-sdk/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
Expand All @@ -17,6 +18,7 @@ on:
- develop
paths:
- 'packages/b2c-vs-extension/**'
- 'packages/b2c-script-types/**'
- 'packages/b2c-tooling-sdk/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dist-deploy
/tmp
node_modules
/coverage
coverage/
oclif.manifest.json
*.tsbuildinfo

Expand Down
33 changes: 33 additions & 0 deletions docs/guide/ide-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,39 @@ require('lspconfig').ts_ls.setup({

If your editor's LSP client is launched outside the repo root (for example, opening a single cartridge subdirectory), point it at the project root so the plugin's auto-discovery walks the right tree.

### Inferring types for undocumented helpers (experimental)

JSDoc-documented functions get full hover/completion support when the annotation names a real Script API type (`@param {dw.customer.Customer}` / `@param {Customer}`), because TypeScript reads those directly — the same happy path the IntelliJ SFCC plugin relies on. Plain, undocumented helpers don't, and neither do the placeholder SFRA annotations that show up constantly in real cartridges (`@param {Object}`, `{obj}`, `{*}`, `{}`): those widen to an uninformative type and silence completion for everything downstream.

Enable the `b2c-dx.features.scriptTypesInferUsage` setting (default: `false`) or pass `inferUsage: true` in the plugin config (`init_options.plugins` for other LSP hosts) to have the plugin infer a plausible type for these cases from how the value is actually used elsewhere in the project — call-site arguments for parameters, return statements for return values — chasing through undocumented call chains (a helper calling a helper calling a helper), multi-hop method chains (`product.getPriceModel().getPrice()`), and intermediate local variables (`var priceModel = product.getPriceModel(); return priceModel.getPrice();`) rather than stopping at the first `any` or placeholder `Object`. Deliberate `@param {any}` / `: any` annotations are still respected and never second-guessed; real `dw.*` JSDoc is left alone too.

`module.superModule` is understood too: in an overlay cartridge that extends a base module (`var base = module.superModule;`), hover and completions on `base` and on values derived from it resolve against the same-path module in the next cartridge down the cartridge path — including recursing into the base module's own undocumented helpers, and across multi-cartridge plugin stacks where intermediate levels re-export the base and add members (`module.exports = base; module.exports.extra = extra;`).

Two more SFRA idioms are covered:

- **Iteration callbacks** — `collections.forEach` / `map` / `filter` / `every` / `some` / `find` / `first` (element-first callback when a predicate is passed; `reduce` and unknown callees are skipped), e.g. `collections.forEach(product.getVariants(), function (variant) {...})`. A callback in argument position has no name to search references for, so `variant` is typed from the element type of the collection travelling alongside it (anything with `iterator()`/`next()`, i.e. `dw.util.Collection` and friends). Manual iterator loops (`var iter = coll.iterator(); while (iter.hasNext()) { var item = iter.next(); }`) and ternary returns like stock `collections.first` (`return it.hasNext() ? it.next() : null`) resolve through the same chain machinery.
- **SFRA naming aliases** — parameters conventionally named `lineItem` / `pli`, `priceModel`, `shippingAddress` / `billingAddress`, `paymentInstrument`, etc. short-circuit ambient matching to the Script API class they hold even when the identifier is not the class's own simple name. CamelCase suffixes are recognized too (`resettingCustomer` → `Customer`, `apiProduct` → `Product`, `currentBasket` → `Basket`), matching the naming style SFRA controllers and helpers use constantly.
- **`instanceof` checks** — a single `param instanceof ProductLineItem` (or `dw.order.ProductLineItem`) in the helper body is treated as concrete class evidence when call sites don't resolve.
- **Controller middleware** — `server.append('Show', function (req, res, next) {...})` needs no inference at all: when a `modules` cartridge is present, the plugin injects its bundled SFRA ambient declarations and TypeScript types `req`/`res`/`next` contextually from the typed `append` signature. Inference deliberately stays out of the way there.

Cross-file inference (call sites in other files, `module.superModule`) needs those files in the same TypeScript project. A `jsconfig.json` that includes all cartridge sources — like the one `b2c setup ide vscode-types` generates — provides that; without one, each open file gets its own inferred project and only same-file usage is visible.

Inferred results are heuristic and clearly labeled:

- Hover text gets an appended `Inferred from usage: <type>` line.
- Member completions synthesized this way are still offered alongside (not instead of) whatever TypeScript already resolved.
- Conflicting call-site argument types cause inference to stay silent rather than union a noisy hover.

This won't recover types TypeScript genuinely can't infer — for example, values that are never called with a consistent, well-typed argument anywhere in the project — and it's off by default because it's new and heuristic.

**Known limitations** — intentionally deferred patterns:

- ES6 `class` syntax / arrow-function module exports
- Destructured function parameters (`function f({a, b})`)
- Destructured return values (`var {a, b} = undocumentedFn()`)
- Constructor inheritance via `Foo.prototype = Base.prototype`
- Guessing individual custom attribute names on `.custom` (only `.custom` itself is usage evidence)

### Notes

- The bundle is version-locked to a Script API release (currently 26.7). Re-run `b2c setup ide vscode-types` after upgrading the CLI to refresh the vendored copy; use `--force` to overwrite existing files if they were previously created. The plugin path returned by `b2c setup ide tsserver-plugin` always points at the bundle shipped with your installed CLI.
Expand Down
16 changes: 15 additions & 1 deletion packages/b2c-script-types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,18 @@ in by the host extension via `tsApi.configurePlugin(...)`. Files outside the
cartridge layout fall straight through to the unwrapped service — non-cartridge
JavaScript and TypeScript in the same workspace see no behavior change.

See [plugin/index.ts](./plugin/index.ts) for the implementation.
See [src/index.ts](./src/index.ts) for the implementation.

### Usage-based type inference (experimental, opt-in)

An undocumented helper function (no JSDoc) gets its parameters and return
value widened to `any` by plain TypeScript inference, and that `any`
propagates to every caller. Passing `inferUsage: true` in the plugin config
(off by default) makes the plugin infer a plausible type for these cases from
how the value is actually used elsewhere in the project — see
[src/usage-inference.ts](./src/usage-inference.ts) (barrel) and the engine
modules under [src/inference/](./src/inference/) — and surface it as an
"Inferred from usage" hover note plus synthesized member completions. It's
heuristic and intentionally conservative: it only kicks in where the checker
has already given up with `any`, never overriding a type TypeScript or JSDoc
already resolved.
9 changes: 9 additions & 0 deletions packages/b2c-script-types/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ export default [
...sharedRules,
},
},
{
// Tests run directly via `node --test` (no bundler/loader), so they're
// plain CommonJS .js files using require() rather than the src/ package's
// ESM-style import syntax.
files: ['test/**/*.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
];
6 changes: 5 additions & 1 deletion packages/b2c-script-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
"lint:agent": "eslint --quiet",
"typecheck:agent": "tsc -p . --noEmit --pretty false",
"format": "prettier --write src",
"format:check": "prettier --check src"
"format:check": "prettier --check src",
"test": "pnpm run build && node --test",
"test:agent": "pnpm run build && node --test --test-reporter=dot",
"test:unit": "pnpm run test",
"test:watch": "pnpm run build && node --test --watch"
},
"devDependencies": {
"@eslint/compat": "catalog:",
Expand Down
Loading
Loading