From 677b56053d49fce4a05cb9b643dce95da1281987 Mon Sep 17 00:00:00 2001 From: Abhinav Pandey Date: Sun, 6 Sep 2026 09:41:16 +0530 Subject: [PATCH] fix(exports): recognize imported and expression-local receivers --- .../src/core/ingestion/ts-js-export-marker.ts | 26 +++++++++++++++++-- .../esm-export-marker.test.ts | 13 ++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/ts-js-export-marker.ts b/gitnexus/src/core/ingestion/ts-js-export-marker.ts index e27433260..94781657d 100644 --- a/gitnexus/src/core/ingestion/ts-js-export-marker.ts +++ b/gitnexus/src/core/ingestion/ts-js-export-marker.ts @@ -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' diff --git a/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts b/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts index 1e9a3cd3c..876bf2bcc 100644 --- a/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts +++ b/gitnexus/test/unit/scope-resolution/esm-export-marker.test.ts @@ -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) => {