Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 82 additions & 15 deletions scripts/javascript-runtime-authority.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import ts from 'typescript';
*
* Parsing the artifact instead of scanning raw text deliberately ignores comments,
* string literals, and template text that merely mention `require()` or `import()`.
* Actual static imports/re-exports, bare CommonJS `require()` calls, and dynamic
* `import()` calls remain fail-closed findings. Literal module specifiers are
* preserved in diagnostics so a verifier failure identifies the dependency edge
* that escaped bundling; computed specifiers remain `undefined` and therefore do
* not acquire an invented interpretation.
* Actual static imports/re-exports, statically recognizable CommonJS loader calls,
* and dynamic `import()` calls remain fail-closed findings. Literal module specifiers
* are preserved in diagnostics so a verifier failure identifies the dependency edge
* that escaped bundling; computed specifiers remain `undefined` and therefore do not
* acquire an invented interpretation.
*
* @param {string} source JavaScript source emitted into the packed artifact.
* @param {string} [filename='bundle.js'] Diagnostic filename for parse failures.
Expand Down Expand Up @@ -42,6 +42,73 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') {
: undefined;
}

/** Remove syntax-only parentheses before classifying an executable reference. */
function unwrapParentheses(expression) {
let current = expression;
while (ts.isParenthesizedExpression(current)) {
current = current.expression;
}
return current;
}

/**
* Return whether an expression statically denotes a CommonJS loader function.
*
* Only intrinsic `require`, exact `module.require`, and comma expressions whose
* final value is one of those references are accepted. Arbitrary object methods
* named `require` remain outside this authority classifier.
*/
function isCommonJsLoaderReference(expression) {
const current = unwrapParentheses(expression);
if (ts.isIdentifier(current) && current.text === 'require') {
return true;
}
if (
ts.isBinaryExpression(current) &&
current.operatorToken.kind === ts.SyntaxKind.CommaToken
) {
return isCommonJsLoaderReference(current.right);
}
if (
ts.isPropertyAccessExpression(current) &&
ts.isIdentifier(unwrapParentheses(current.expression)) &&
unwrapParentheses(current.expression).text === 'module' &&
current.name.text === 'require'
) {
return true;
}
if (
ts.isElementAccessExpression(current) &&
ts.isIdentifier(unwrapParentheses(current.expression)) &&
unwrapParentheses(current.expression).text === 'module' &&
literalSpecifier(current.argumentExpression) === 'require'
) {
return true;
}
return false;
}

/**
* Resolve the specifier-bearing argument for a recognized CommonJS call.
*
* A matched call may legitimately have no literal argument, so the result uses
* an explicit object instead of overloading `undefined` as the no-match signal.
*/
function commonJsCall(node) {
const callee = unwrapParentheses(node.expression);
if (isCommonJsLoaderReference(callee)) {
return { specifierExpression: node.arguments[0] };
}
if (
ts.isPropertyAccessExpression(callee) &&
callee.name.text === 'call' &&
isCommonJsLoaderReference(callee.expression)
) {
return { specifierExpression: node.arguments[1] };
}
return null;
}

/** @param {import('typescript').Node} node Parsed JavaScript node. */
function visit(node) {
if (ts.isImportDeclaration(node) && node.moduleSpecifier) {
Expand All @@ -63,15 +130,15 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') {
offset: node.getStart(sourceFile),
specifier: literalSpecifier(node.arguments[0]),
});
} else if (
ts.isIdentifier(node.expression) &&
node.expression.text === 'require'
) {
findings.push({
kind: 'commonjs-require',
offset: node.getStart(sourceFile),
specifier: literalSpecifier(node.arguments[0]),
});
} else {
const commonJs = commonJsCall(node);
if (commonJs) {
findings.push({
kind: 'commonjs-require',
offset: node.getStart(sourceFile),
specifier: literalSpecifier(commonJs.specifierExpression),
});
}
}
}

Expand All @@ -80,4 +147,4 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') {

visit(sourceFile);
return Object.freeze(findings.map((finding) => Object.freeze(finding)));
}
}
28 changes: 27 additions & 1 deletion scripts/javascript-runtime-authority.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,35 @@ test('reports executable module authority with actionable specifiers', () => {
);
});

test('reports statically recognizable indirect CommonJS loader calls', () => {
const source = [
"const first = (0, require)('comma-package');",
"const second = require.call(undefined, 'call-package');",
"const third = module.require('module-package');",
"const fourth = module['require']('element-package');",
'const fifth = (0, require)(runtimePackageName);',
'const ordinary = { require() { return "ordinary-method"; } };',
"ordinary.require('not-commonjs-authority');",
'void [first, second, third, fourth, fifth];',
].join('\n');

assert.deepEqual(
findRuntimeModuleAuthority(source, 'indirect-commonjs.js').map(
({ kind, specifier }) => ({ kind, specifier }),
),
[
{ kind: 'commonjs-require', specifier: 'comma-package' },
{ kind: 'commonjs-require', specifier: 'call-package' },
{ kind: 'commonjs-require', specifier: 'module-package' },
{ kind: 'commonjs-require', specifier: 'element-package' },
{ kind: 'commonjs-require', specifier: undefined },
],
);
});

test('fails closed when emitted JavaScript is syntactically invalid', () => {
assert.throws(
() => findRuntimeModuleAuthority('const = ;', 'broken.js'),
/broken\.js is not valid JavaScript/u,
);
});
});
Loading