Skip to content

Clean up amalgamatedDuplicates #27285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 commits merged into from
Oct 1, 2018
Merged
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
114 changes: 58 additions & 56 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,19 @@ namespace ts {
const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);

const globals = createSymbolTable();
let amalgamatedDuplicates: Map<{ firstFile: SourceFile, secondFile: SourceFile, firstFileInstances: Map<{ instances: Node[], blockScoped: boolean }>, secondFileInstances: Map<{ instances: Node[], blockScoped: boolean }> }> | undefined;
interface DuplicateInfoForSymbol {
readonly firstFileLocations: Node[];
readonly secondFileLocations: Node[];
readonly isBlockScoped: boolean;
}
interface DuplicateInfoForFiles {
readonly firstFile: SourceFile;
readonly secondFile: SourceFile;
/** Key is symbol name. */
readonly conflictingSymbols: Map<DuplicateInfoForSymbol>;
}
/** Key is "/path/to/a.ts|/path/to/b.ts". */
let amalgamatedDuplicates: Map<DuplicateInfoForFiles> | undefined;
const reverseMappedCache = createMap<Type | undefined>();
let ambientModulesCache: Symbol[] | undefined;
/**
Expand Down Expand Up @@ -883,50 +895,45 @@ namespace ts {
: Diagnostics.Duplicate_identifier_0;
const sourceSymbolFile = source.declarations && getSourceFileOfNode(source.declarations[0]);
const targetSymbolFile = target.declarations && getSourceFileOfNode(target.declarations[0]);
const symbolName = symbolToString(source);

// Collect top-level duplicate identifier errors into one mapping, so we can then merge their diagnostics if there are a bunch
if (sourceSymbolFile && targetSymbolFile && amalgamatedDuplicates && !isEitherEnum && sourceSymbolFile !== targetSymbolFile) {
const firstFile = comparePaths(sourceSymbolFile.path, targetSymbolFile.path) === Comparison.LessThan ? sourceSymbolFile : targetSymbolFile;
const secondFile = firstFile === sourceSymbolFile ? targetSymbolFile : sourceSymbolFile;
const cacheKey = `${firstFile.path}|${secondFile.path}`;
const existing = amalgamatedDuplicates.get(cacheKey) || { firstFile, secondFile, firstFileInstances: createMap(), secondFileInstances: createMap() };
const symbolName = symbolToString(source);
const firstInstanceList = existing.firstFileInstances.get(symbolName) || { instances: [], blockScoped: isEitherBlockScoped };
const secondInstanceList = existing.secondFileInstances.get(symbolName) || { instances: [], blockScoped: isEitherBlockScoped };

forEach(source.declarations, node => {
const errorNode = (getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? getNameOfExpando(node) : getNameOfDeclaration(node)) || node;
const targetList = sourceSymbolFile === firstFile ? firstInstanceList : secondInstanceList;
targetList.instances.push(errorNode);
});
forEach(target.declarations, node => {
const errorNode = (getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? getNameOfExpando(node) : getNameOfDeclaration(node)) || node;
const targetList = targetSymbolFile === firstFile ? firstInstanceList : secondInstanceList;
targetList.instances.push(errorNode);
});

existing.firstFileInstances.set(symbolName, firstInstanceList);
existing.secondFileInstances.set(symbolName, secondInstanceList);
amalgamatedDuplicates.set(cacheKey, existing);
return target;
const filesDuplicates = getOrUpdate<DuplicateInfoForFiles>(amalgamatedDuplicates, `${firstFile.path}|${secondFile.path}`, () =>
({ firstFile, secondFile, conflictingSymbols: createMap() }));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to see DuplicateInfoForFiles as an assertion on the object literal: { firstFile, secondFile, conflictingSymbols: createMap() } as DuplicateInfoForFiles

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would conflict with https://palantir.github.io/tslint/rules/no-object-literal-type-assertion/ -- it's unsafe to cast an object literal because that allows misspelled properties.

const conflictingSymbolInfo = getOrUpdate<DuplicateInfoForSymbol>(filesDuplicates.conflictingSymbols, symbolName, () =>
({ isBlockScoped: isEitherBlockScoped, firstFileLocations: [], secondFileLocations: [] }));
addDuplicateLocations(conflictingSymbolInfo.firstFileLocations, source);
addDuplicateLocations(conflictingSymbolInfo.secondFileLocations, target);
}
else {
addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target);
addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source);
}
const symbolName = symbolToString(source);
addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target);
addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source);
}
return target;

function addDuplicateLocations(locs: Node[], symbol: Symbol): void {
for (const decl of symbol.declarations) {
pushIfUnique(locs, (getExpandoInitializer(decl, /*isPrototypeAssignment*/ false) ? getNameOfExpando(decl) : getNameOfDeclaration(decl)) || decl);
}
}
}

function addDuplicateDeclarationErrorsForSymbols(target: Symbol, message: DiagnosticMessage, symbolName: string, source: Symbol) {
forEach(target.declarations, node => {
const errorNode = (getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? getNameOfExpando(node) : getNameOfDeclaration(node)) || node;
addDuplicateDeclarationError(errorNode, message, symbolName, source.declarations && source.declarations[0]);
addDuplicateDeclarationError(errorNode, message, symbolName, source.declarations);
});
}

function addDuplicateDeclarationError(errorNode: Node, message: DiagnosticMessage, symbolName: string, relatedNode: Node | undefined) {
function addDuplicateDeclarationError(errorNode: Node, message: DiagnosticMessage, symbolName: string, relatedNodes: ReadonlyArray<Node> | undefined) {
const err = lookupOrIssueError(errorNode, message, symbolName);
if (relatedNode && length(err.relatedInformation) < 5) {
for (const relatedNode of relatedNodes || emptyArray) {
err.relatedInformation = err.relatedInformation || [];
if (length(err.relatedInformation) >= 5) continue;
addRelatedInfo(err, !length(err.relatedInformation) ? createDiagnosticForNode(relatedNode, Diagnostics._0_was_also_declared_here, symbolName) : createDiagnosticForNode(relatedNode, Diagnostics.and_here));
}
}
Expand Down Expand Up @@ -28842,38 +28849,33 @@ namespace ts {
}
}

amalgamatedDuplicates.forEach(({ firstFile, secondFile, firstFileInstances, secondFileInstances }) => {
const conflictingKeys = arrayFrom(firstFileInstances.keys());
amalgamatedDuplicates.forEach(({ firstFile, secondFile, conflictingSymbols }) => {
// If not many things conflict, issue individual errors
if (conflictingKeys.length < 8) {
addErrorsForDuplicates(firstFileInstances, secondFileInstances);
addErrorsForDuplicates(secondFileInstances, firstFileInstances);
return;
if (conflictingSymbols.size < 8) {
conflictingSymbols.forEach(({ isBlockScoped, firstFileLocations, secondFileLocations }, symbolName) => {
const message = isBlockScoped ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0;
for (const node of firstFileLocations) {
addDuplicateDeclarationError(node, message, symbolName, secondFileLocations);
}
for (const node of secondFileLocations) {
addDuplicateDeclarationError(node, message, symbolName, firstFileLocations);
}
});
}
else {
// Otherwise issue top-level error since the files appear very identical in terms of what they contain
const list = arrayFrom(conflictingSymbols.keys()).join(", ");
diagnostics.add(addRelatedInfo(
createDiagnosticForNode(firstFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list),
createDiagnosticForNode(secondFile, Diagnostics.Conflicts_are_in_this_file)
));
diagnostics.add(addRelatedInfo(
createDiagnosticForNode(secondFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list),
createDiagnosticForNode(firstFile, Diagnostics.Conflicts_are_in_this_file)
));
}
// Otheriwse issue top-level error since the files appear very identical in terms of what they appear
const list = conflictingKeys.join(", ");
diagnostics.add(addRelatedInfo(
createDiagnosticForNode(firstFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list),
createDiagnosticForNode(secondFile, Diagnostics.Conflicts_are_in_this_file)
));
diagnostics.add(addRelatedInfo(
createDiagnosticForNode(secondFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list),
createDiagnosticForNode(firstFile, Diagnostics.Conflicts_are_in_this_file)
));
});
amalgamatedDuplicates = undefined;

function addErrorsForDuplicates(secondFileInstances: Map<{ instances: Node[]; blockScoped: boolean; }>, firstFileInstances: Map<{ instances: Node[]; blockScoped: boolean; }>) {
secondFileInstances.forEach((locations, symbolName) => {
const firstFileEquivalent = firstFileInstances.get(symbolName)!;
const message = locations.blockScoped
? Diagnostics.Cannot_redeclare_block_scoped_variable_0
: Diagnostics.Duplicate_identifier_0;
locations.instances.forEach(node => {
addDuplicateDeclarationError(node, message, symbolName, firstFileEquivalent.instances[0]);
});
});
}
}

function checkExternalEmitHelpers(location: Node, helpers: ExternalEmitHelpers) {
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8369,4 +8369,16 @@ namespace ts {
export function isJsonEqual(a: unknown, b: unknown): boolean {
return a === b || typeof a === "object" && a !== null && typeof b === "object" && b !== null && equalOwnProperties(a as MapLike<unknown>, b as MapLike<unknown>, isJsonEqual);
}

export function getOrUpdate<T>(map: Map<T>, key: string, getDefault: () => T): T {
const got = map.get(key);
if (got === undefined) {
const value = getDefault();
map.set(key, value);
return value;
}
else {
return got;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/dir/a.ts(1,14): error TS2451: Cannot redeclare block-scoped variable 'x'.
/dir/b.ts(4,18): error TS2451: Cannot redeclare block-scoped variable 'x'.
/dir/b.ts(8,18): error TS2451: Cannot redeclare block-scoped variable 'x'.


==== /dir/a.ts (1 errors) ====
export const x = 0;
~
!!! error TS2451: Cannot redeclare block-scoped variable 'x'.
!!! related TS6203 /dir/b.ts:4:18: 'x' was also declared here.
!!! related TS6204 /dir/b.ts:8:18: and here.

==== /dir/b.ts (2 errors) ====
export {};

declare module "./a" {
export const x = 0;
~
!!! error TS2451: Cannot redeclare block-scoped variable 'x'.
!!! related TS6203 /dir/a.ts:1:14: 'x' was also declared here.
}

declare module "../dir/a" {
export const x = 0;
~
!!! error TS2451: Cannot redeclare block-scoped variable 'x'.
!!! related TS6203 /dir/a.ts:1:14: 'x' was also declared here.
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//// [tests/cases/compiler/duplicateIdentifierRelatedSpans_moduleAugmentation.ts] ////

//// [a.ts]
export const x = 0;

//// [b.ts]
export {};

declare module "./a" {
export const x = 0;
}

declare module "../dir/a" {
export const x = 0;
}


//// [a.js]
"use strict";
exports.__esModule = true;
exports.x = 0;
//// [b.js]
"use strict";
exports.__esModule = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
=== /dir/a.ts ===
export const x = 0;
>x : Symbol(x, Decl(a.ts, 0, 12))

=== /dir/b.ts ===
export {};

declare module "./a" {
>"./a" : Symbol("/dir/a", Decl(a.ts, 0, 0), Decl(b.ts, 0, 10), Decl(b.ts, 4, 1))

export const x = 0;
>x : Symbol(x, Decl(b.ts, 3, 16))
}

declare module "../dir/a" {
>"../dir/a" : Symbol("/dir/a", Decl(a.ts, 0, 0), Decl(b.ts, 0, 10), Decl(b.ts, 4, 1))

export const x = 0;
>x : Symbol(x, Decl(b.ts, 7, 16))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
=== /dir/a.ts ===
export const x = 0;
>x : 0
>0 : 0

=== /dir/b.ts ===
export {};

declare module "./a" {
>"./a" : typeof import("/dir/a")

export const x = 0;
>x : 0
>0 : 0
}

declare module "../dir/a" {
>"../dir/a" : typeof import("/dir/a")

export const x = 0;
>x : 0
>0 : 0
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @Filename: /dir/a.ts
export const x = 0;

// @Filename: /dir/b.ts
export {};

declare module "./a" {
export const x = 0;
}

declare module "../dir/a" {
export const x = 0;
}