fix(javascript): keep configuration setting reads out of endpoints (#2402)

* fix(javascript): keep configuration setting reads out of endpoints

Static endpoint extraction reads the first literal argument of any call
named get as an HTTP request path unless the receiver is a provable keyed
collection. VS Code extension bundles read settings through
`workspace.getConfiguration(section).get(key)`, so every setting key
became a network endpoint: the bundled Git extension's dist/main.js
reported 173 network endpoints, nearly all of them setting names such as
`autofetchPeriod` and `blame.statusBarItem.enabled`.

The lexical receiver classifier now records a `configuration` fact for a
`.get` call whose receiver is a `getConfiguration(...)` call, inline or
through a binding whose only visible write is one unconditional
initializer. Minifiers emit `let` for these bindings, so the proof relies
on the recorded writes rather than the declaration kind, as tRPC client
root resolution already does. Endpoint extraction skips non-URL literals
for that fact in both application and web bundle analysis. Reassigned,
conditional, and shadowed receivers, URL or path literals, and other
clients such as `api.get("users")` keep their endpoints.

The receiver fact union was duplicated in three modules; consumers now
import the classifier's type.

On the Git extension bundle, network endpoints drop from 173 to 7 with
unchanged analysis time (2.1 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): require the workspace host for configuration receivers

A bare or differently scoped `getConfiguration(...)` may be an
application's own HTTP client factory, so `.get("users")` on its result
lost its endpoint. Only `workspace.getConfiguration(...)` and
`<x>.workspace.getConfiguration(...)` now produce the configuration fact.
Minifiers keep property names, so the bundled VS Code Git extension still
reports 7 network endpoints instead of 173.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): recognize default VS Code namespace imports

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 21:45:07 +08:00
1 parent 5d12a0b4b6
commit 23f72141d5
4 files changed
+159 -34

No files matched your search

@@ -247,19 +247,24 @@ export function* classifyParsedJavaScriptRpcClientRootsSteps(
return roots;
}
/** Receiver identity facts for the overloaded `.open` syntax only. */
/**
* Receiver identity facts for the overloaded `.open` syntax, plus
* `configuration` for `.get` reads of a `getConfiguration(...)` result.
*/
export type JavaScriptOpenReceiverFact =
| "window"
| "indexed-db"
| "cache-storage"
| "local"
| "local-indexed-db"
| "local-cache-storage";
| "local-cache-storage"
| "configuration";
/**
* Resolve only the lexical receiver identity needed to distinguish browser
* `window.open` from XHR and global storage `.open` calls. This deliberately
* builds definitions without running semantic value or provenance analysis.
* `window.open` from XHR and global storage `.open` calls, and configuration
* `.get` reads from HTTP client calls. This deliberately builds definitions
* without running semantic value or provenance analysis.
*/
export const classifyParsedJavaScriptOpenReceivers = (
file: ParsedJavaScriptSource,
@@ -278,14 +283,19 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
(!t.isMemberExpression(node.callee) &&
!t.isOptionalMemberExpression(node.callee)) ||
semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
) !== "open" ||
!t.isIdentifier(node.callee.object)
!t.isOptionalMemberExpression(node.callee))
)
return;
const method = semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
);
if (method === "get") {
if (isConfigurationReceiver(state, node.callee.object))
facts.set(node.start ?? -1, "configuration");
return;
}
if (method !== "open" || !t.isIdentifier(node.callee.object)) return;
const receiver = node.callee.object.name;
const binding = resolveSemanticBindingState(state, node, receiver);
if (binding === undefined) {
@@ -351,6 +361,82 @@ export function* classifyParsedJavaScriptOpenReceiversSteps(
return facts;
}
/**
* Whether a call spells VS Code's `workspace.getConfiguration(section)`, whose
* result `.get` reads setting keys rather than request paths. The `workspace`
* member is required: a bare or differently scoped `getConfiguration` may be
* an application's own HTTP client factory. Minifiers keep property names, so
* bundled `x.workspace.getConfiguration` still matches.
*/
const isConfigurationAccessorCall = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
): boolean => {
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return false;
const callee = node.callee;
if (
(!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) ||
semanticStaticPropertyKey(callee.property, callee.computed) !==
"getConfiguration"
)
return false;
const host = callee.object;
const matchesSpelling = t.isIdentifier(host)
? host.name === "workspace"
: (t.isMemberExpression(host) || t.isOptionalMemberExpression(host)) &&
semanticStaticPropertyKey(host.property, host.computed) === "workspace";
if (!matchesSpelling) return false;
const root = t.isIdentifier(host)
? host
: t.isMemberExpression(host) || t.isOptionalMemberExpression(host)
? host.object
: undefined;
if (!t.isIdentifier(root)) return false;
const binding = resolveSemanticBindingState(state, root, root.name);
if (binding === undefined)
return !semanticResolutionBlocked(state, root, root.name);
const initializer = semanticStableInitializer(binding, state);
const origins = [
...binding.directOrigins,
...(initializer === undefined
? []
: [semanticRequireOrigin(initializer.node, state)?.origin]),
];
return origins.some(
(origin) =>
origin?.specifier === "vscode" &&
(t.isIdentifier(host)
? origin.importedPath.at(-1) === "workspace" ||
initializer?.projection.at(-1) === "workspace"
: origin.importedPath.length === 0 ||
(origin.importedPath.length === 1 &&
(origin.importedPath[0] === "*" ||
origin.importedPath[0] === "default"))),
);
};
/**
* Whether a `.get` receiver is a configuration accessor result, either inline
* or through a binding whose only visible write is one unconditional
* initializer. Minifiers emit `let` for such bindings, so declaration kind
* alone does not decide.
*/
const isConfigurationReceiver = (
state: JavaScriptSemanticAnalysisState,
receiver: t.Node,
): boolean => {
if (isConfigurationAccessorCall(state, receiver)) return true;
if (!t.isIdentifier(receiver)) return false;
const binding = resolveSemanticBindingState(state, receiver, receiver.name);
const initializer = semanticStableInitializer(binding, state);
return (
initializer !== undefined &&
initializer.projection.length === 0 &&
isConfigurationAccessorCall(state, initializer.node)
);
};
const openGlobalFact = (
name: string,
): JavaScriptOpenReceiverFact | undefined => {