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:
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 => {
|
||||
|
||||
Reference in new issue
Block a user