diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ac46ee7e16347..6320ff7d12861 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6029,14 +6029,19 @@ namespace ts { serializeAsNamespaceDeclaration(realMembers, localName, modifierFlags, !!(symbol.flags & (SymbolFlags.Function | SymbolFlags.Assignment))); } if (length(mergedMembers)) { + const containingFile = getSourceFileOfNode(context.enclosingDeclaration); const localName = getInternalSymbolName(symbol, symbolName); const nsBody = createModuleBlock([createExportDeclaration( /*decorators*/ undefined, /*modifiers*/ undefined, - createNamedExports(map(filter(mergedMembers, n => n.escapedName !== InternalSymbolName.ExportEquals), s => { + createNamedExports(mapDefined(filter(mergedMembers, n => n.escapedName !== InternalSymbolName.ExportEquals), s => { const name = unescapeLeadingUnderscores(s.escapedName); const localName = getInternalSymbolName(s, name); const aliasDecl = s.declarations && getDeclarationOfAliasSymbol(s); + if (containingFile && (aliasDecl ? containingFile !== getSourceFileOfNode(aliasDecl) : !some(s.declarations, d => getSourceFileOfNode(d) === containingFile))) { + context.tracker?.reportNonlocalAugmentation?.(containingFile, symbol, s); + return undefined; + } const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); includePrivateSymbol(target || s); const targetName = target ? getInternalSymbolName(target, unescapeLeadingUnderscores(target.escapedName)) : localName; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 4a7c6e8b461f4..17b3dd829bd53 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4388,6 +4388,14 @@ "category": "Error", "code": 6231 }, + "Declaration augments declaration in another file. This cannot be serialized.": { + "category": "Error", + "code": 6232 + }, + "This is the declaration being augmented. Consider moving the augmenting declaration into the same file.": { + "category": "Error", + "code": 6233 + }, "Projects to reference": { "category": "Message", diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 70f70a98c3e70..413ff8f8cd2e5 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -76,7 +76,8 @@ namespace ts { reportLikelyUnsafeImportRequiredError, moduleResolverHost: host, trackReferencedAmbientModule, - trackExternalModuleSymbolOfImportTypeNode + trackExternalModuleSymbolOfImportTypeNode, + reportNonlocalAugmentation }; let errorNameNode: DeclarationName | undefined; @@ -190,6 +191,17 @@ namespace ts { } } + function reportNonlocalAugmentation(containingFile: SourceFile, parentSymbol: Symbol, symbol: Symbol) { + const primaryDeclaration = find(parentSymbol.declarations, d => getSourceFileOfNode(d) === containingFile)!; + const augmentingDeclarations = filter(symbol.declarations, d => getSourceFileOfNode(d) !== containingFile); + for (const augmentations of augmentingDeclarations) { + context.addDiagnostic(addRelatedInfo( + createDiagnosticForNode(augmentations, Diagnostics.Declaration_augments_declaration_in_another_file_This_cannot_be_serialized), + createDiagnosticForNode(primaryDeclaration, Diagnostics.This_is_the_declaration_being_augmented_Consider_moving_the_augmenting_declaration_into_the_same_file) + )); + } + } + function transformDeclarationsForJS(sourceFile: SourceFile, bundled?: boolean) { const oldDiag = getSymbolAccessibilityDiagnostic; getSymbolAccessibilityDiagnostic = (s) => ({ diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8e781b23ee762..70bc9b01528ce 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6468,6 +6468,7 @@ namespace ts { moduleResolverHost?: ModuleSpecifierResolutionHost & { getCommonSourceDirectory(): string }; trackReferencedAmbientModule?(decl: ModuleDeclaration, symbol: Symbol): void; trackExternalModuleSymbolOfImportTypeNode?(symbol: Symbol): void; + reportNonlocalAugmentation?(containingFile: SourceFile, parentSymbol: Symbol, augmentingSymbol: Symbol): void; } export interface TextSpan { diff --git a/tests/baselines/reference/jsDeclarationsCrossfileMerge.errors.txt b/tests/baselines/reference/jsDeclarationsCrossfileMerge.errors.txt new file mode 100644 index 0000000000000..cd0bc2e0a3d9f --- /dev/null +++ b/tests/baselines/reference/jsDeclarationsCrossfileMerge.errors.txt @@ -0,0 +1,17 @@ +tests/cases/conformance/jsdoc/declarations/index.js(4,1): error TS6232: Declaration augments declaration in another file. This cannot be serialized. + + +==== tests/cases/conformance/jsdoc/declarations/index.js (1 errors) ==== + const m = require("./exporter"); + + module.exports = m.default; + module.exports.memberName = "thing"; + ~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS6232: Declaration augments declaration in another file. This cannot be serialized. +!!! related TS6233 /.src/tests/cases/conformance/jsdoc/declarations/exporter.js:1:10: This is the declaration being augmented. Consider moving the augmenting declaration into the same file. + +==== tests/cases/conformance/jsdoc/declarations/exporter.js (0 errors) ==== + function validate() {} + + export default validate; + \ No newline at end of file diff --git a/tests/baselines/reference/jsDeclarationsCrossfileMerge.js b/tests/baselines/reference/jsDeclarationsCrossfileMerge.js new file mode 100644 index 0000000000000..9e99ba43fc55e --- /dev/null +++ b/tests/baselines/reference/jsDeclarationsCrossfileMerge.js @@ -0,0 +1,28 @@ +//// [tests/cases/conformance/jsdoc/declarations/jsDeclarationsCrossfileMerge.ts] //// + +//// [index.js] +const m = require("./exporter"); + +module.exports = m.default; +module.exports.memberName = "thing"; + +//// [exporter.js] +function validate() {} + +export default validate; + + +//// [exporter.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +function validate() { } +exports.default = validate; +//// [index.js] +var m = require("./exporter"); +module.exports = m.default; +module.exports.memberName = "thing"; + + +//// [index.d.ts] +declare const _exports: typeof import("./exporter").default; +export = _exports; diff --git a/tests/baselines/reference/jsDeclarationsCrossfileMerge.symbols b/tests/baselines/reference/jsDeclarationsCrossfileMerge.symbols new file mode 100644 index 0000000000000..99455853c4c1a --- /dev/null +++ b/tests/baselines/reference/jsDeclarationsCrossfileMerge.symbols @@ -0,0 +1,28 @@ +=== tests/cases/conformance/jsdoc/declarations/index.js === +const m = require("./exporter"); +>m : Symbol(m, Decl(index.js, 0, 5)) +>require : Symbol(require) +>"./exporter" : Symbol("tests/cases/conformance/jsdoc/declarations/exporter", Decl(exporter.js, 0, 0)) + +module.exports = m.default; +>module.exports : Symbol("tests/cases/conformance/jsdoc/declarations/index", Decl(index.js, 0, 0)) +>module : Symbol(export=, Decl(index.js, 0, 32)) +>exports : Symbol(export=, Decl(index.js, 0, 32)) +>m.default : Symbol(default, Decl(exporter.js, 0, 22)) +>m : Symbol(m, Decl(index.js, 0, 5)) +>default : Symbol(default, Decl(exporter.js, 0, 22)) + +module.exports.memberName = "thing"; +>module.exports.memberName : Symbol(memberName) +>module.exports : Symbol(memberName, Decl(index.js, 2, 27)) +>module : Symbol(module, Decl(index.js, 0, 32)) +>exports : Symbol("tests/cases/conformance/jsdoc/declarations/index", Decl(index.js, 0, 0)) +>memberName : Symbol(memberName, Decl(index.js, 2, 27)) + +=== tests/cases/conformance/jsdoc/declarations/exporter.js === +function validate() {} +>validate : Symbol(validate, Decl(exporter.js, 0, 0)) + +export default validate; +>validate : Symbol(validate, Decl(exporter.js, 0, 0)) + diff --git a/tests/baselines/reference/jsDeclarationsCrossfileMerge.types b/tests/baselines/reference/jsDeclarationsCrossfileMerge.types new file mode 100644 index 0000000000000..8de95cf8567eb --- /dev/null +++ b/tests/baselines/reference/jsDeclarationsCrossfileMerge.types @@ -0,0 +1,32 @@ +=== tests/cases/conformance/jsdoc/declarations/index.js === +const m = require("./exporter"); +>m : typeof import("tests/cases/conformance/jsdoc/declarations/exporter") +>require("./exporter") : typeof import("tests/cases/conformance/jsdoc/declarations/exporter") +>require : any +>"./exporter" : "./exporter" + +module.exports = m.default; +>module.exports = m.default : typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default +>module.exports : typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default +>module : { "\"tests/cases/conformance/jsdoc/declarations/index\"": typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default; } +>exports : typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default +>m.default : { (): void; memberName: string; } +>m : typeof import("tests/cases/conformance/jsdoc/declarations/exporter") +>default : { (): void; memberName: string; } + +module.exports.memberName = "thing"; +>module.exports.memberName = "thing" : "thing" +>module.exports.memberName : string +>module.exports : typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default +>module : { "\"tests/cases/conformance/jsdoc/declarations/index\"": typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default; } +>exports : typeof import("tests/cases/conformance/jsdoc/declarations/exporter").default +>memberName : string +>"thing" : "thing" + +=== tests/cases/conformance/jsdoc/declarations/exporter.js === +function validate() {} +>validate : typeof validate + +export default validate; +>validate : typeof validate + diff --git a/tests/cases/conformance/jsdoc/declarations/jsDeclarationsCrossfileMerge.ts b/tests/cases/conformance/jsdoc/declarations/jsDeclarationsCrossfileMerge.ts new file mode 100644 index 0000000000000..7f1624e6d8738 --- /dev/null +++ b/tests/cases/conformance/jsdoc/declarations/jsDeclarationsCrossfileMerge.ts @@ -0,0 +1,16 @@ +// @allowJs: true +// @checkJs: true +// @target: es5 +// @lib: es6 +// @outDir: ./out +// @declaration: true +// @filename: index.js +const m = require("./exporter"); + +module.exports = m.default; +module.exports.memberName = "thing"; + +// @filename: exporter.js +function validate() {} + +export default validate;