fix(exports): recognize imported and expression-local receivers

This commit is contained in:
Abhinav Pandey 2026-09-06 09:41:16 +05:30
parent a1c7706886
commit 677b56053d
No known key found for this signature in database
2 changed files with 37 additions and 2 deletions

View file

@ -57,18 +57,33 @@ function bindsReceiver(node: SyntaxNode | null, name: string): boolean {
if (node.type === 'assignment_pattern')
return bindsReceiver(node.childForFieldName('left'), name);
if (node.type === 'pair_pattern') return bindsReceiver(node.childForFieldName('value'), name);
if (node.type === 'import_specifier') {
return bindsReceiver(node.childForFieldName('alias') ?? node.childForFieldName('name'), name);
}
if (node.type === 'required_parameter' || node.type === 'optional_parameter') {
return bindsReceiver(node.childForFieldName('pattern'), name);
}
return (
['formal_parameters', 'object_pattern', 'array_pattern', 'rest_pattern'].includes(node.type) &&
node.namedChildren.some((child) => bindsReceiver(child, name))
[
'formal_parameters',
'object_pattern',
'array_pattern',
'rest_pattern',
'import_clause',
'named_imports',
'namespace_import',
].includes(node.type) && node.namedChildren.some((child) => bindsReceiver(child, name))
);
}
/** A locally bound `module`/`exports` is not Node's export receiver. */
function isExportReceiverShadowed(node: SyntaxNode, name: string): boolean {
for (let scope = node.parent; scope !== null; scope = scope.parent) {
if (
['function_expression', 'generator_function', 'class'].includes(scope.type) &&
scope.childForFieldName('name')?.text === name
)
return true;
if (
bindsReceiver(scope.childForFieldName('parameters'), name) ||
bindsReceiver(scope.childForFieldName('parameter'), name)
@ -81,6 +96,13 @@ function isExportReceiverShadowed(node: SyntaxNode, name: string): boolean {
? statement.childForFieldName('declaration')
: statement;
if (declaration === null) continue;
if (
declaration.type === 'import_statement' &&
declaration.namedChildren.some(
(child) => child.type === 'import_clause' && bindsReceiver(child, name),
)
)
return true;
if (
declaration.type === 'lexical_declaration' ||
declaration.type === 'variable_declaration'

View file

@ -174,6 +174,10 @@ describe('@declaration.is-exported (JavaScript emitter)', () => {
'function wrapper() { { module.exports = {}; let module; } }',
'function wrapper({ receiver: module }) { module.exports = {}; }',
'const module = {}; module.exports = { alpha() {} };',
"import module from './other'; module.exports = {};",
"import { receiver as exports } from './other'; exports.alpha = 1;",
"import * as module from './other'; module.exports = {};",
'const wrapper = function module() { module.exports = {}; };',
])('ignores a shadowed CommonJS receiver: %s', (source) => {
for (const [emit, file] of [
[emitJsScopeCaptures, 'x.js'],
@ -198,6 +202,15 @@ describe('@declaration.is-exported (JavaScript emitter)', () => {
expect(v.hidden).toBeUndefined();
});
it('an imported source name does not shadow a different local alias', () => {
const v = verdicts(
emitJsScopeCaptures,
"import { module as other } from './other'; module.exports = {}; function hidden() {}",
'x.js',
);
expect(v.hidden).toBeUndefined();
});
it.each(["module['exports']", 'module["exports"]'])(
'recognizes %s object methods and properties as exports',
(target) => {