Skip to content

Commit 7a71887

Browse files
authored
Build better import paths for declaration emit/typeToString from reexports if possible (#27340)
* Build better import paths from reexports if possible, issue error on node_modules import generation * Small refactorings * Add file-by-file cacheing * Minor cleanups * Adjust error message
1 parent 12e371b commit 7a71887

29 files changed

+937
-34
lines changed

src/compiler/checker.ts

+78-4
Original file line numberDiff line numberDiff line change
@@ -2599,6 +2599,44 @@ namespace ts {
25992599
return getMergedSymbol(symbol.parent && getLateBoundSymbol(symbol.parent));
26002600
}
26012601

2602+
function getAlternativeContainingModules(symbol: Symbol, enclosingDeclaration: Node): Symbol[] {
2603+
const containingFile = getSourceFileOfNode(enclosingDeclaration);
2604+
const id = "" + getNodeId(containingFile);
2605+
const links = getSymbolLinks(symbol);
2606+
let results: Symbol[] | undefined;
2607+
if (links.extendedContainersByFile && (results = links.extendedContainersByFile.get(id))) {
2608+
return results;
2609+
}
2610+
if (containingFile && containingFile.imports) {
2611+
// Try to make an import using an import already in the enclosing file, if possible
2612+
for (const importRef of containingFile.imports) {
2613+
if (nodeIsSynthesized(importRef)) continue; // Synthetic names can't be resolved by `resolveExternalModuleName` - they'll cause a debug assert if they error
2614+
const resolvedModule = resolveExternalModuleName(enclosingDeclaration, importRef);
2615+
if (!resolvedModule) continue;
2616+
const ref = getAliasForSymbolInContainer(resolvedModule, symbol);
2617+
if (!ref) continue;
2618+
results = append(results, resolvedModule);
2619+
}
2620+
if (length(results)) {
2621+
(links.extendedContainersByFile || (links.extendedContainersByFile = createMap())).set(id, results!);
2622+
return results!;
2623+
}
2624+
}
2625+
if (links.extendedContainers) {
2626+
return links.extendedContainers;
2627+
}
2628+
// No results from files already being imported by this file - expand search (expensive, but not location-specific, so cached)
2629+
const otherFiles = host.getSourceFiles();
2630+
for (const file of otherFiles) {
2631+
if (!isExternalModule(file)) continue;
2632+
const sym = getSymbolOfNode(file);
2633+
const ref = getAliasForSymbolInContainer(sym, symbol);
2634+
if (!ref) continue;
2635+
results = append(results, sym);
2636+
}
2637+
return links.extendedContainers = results || emptyArray;
2638+
}
2639+
26022640
/**
26032641
* Attempts to find the symbol corresponding to the container a symbol is in - usually this
26042642
* is just its' `.parent`, but for locals, this value is `undefined`
@@ -2607,10 +2645,12 @@ namespace ts {
26072645
const container = getParentOfSymbol(symbol);
26082646
if (container) {
26092647
const additionalContainers = mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer);
2648+
const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration);
26102649
if (enclosingDeclaration && getAccessibleSymbolChain(container, enclosingDeclaration, SymbolFlags.Namespace, /*externalOnly*/ false)) {
2611-
return concatenate([container], additionalContainers); // This order expresses a preference for the real container if it is in scope
2650+
return concatenate(concatenate([container], additionalContainers), reexportContainers); // This order expresses a preference for the real container if it is in scope
26122651
}
2613-
return append(additionalContainers, container);
2652+
const res = append(additionalContainers, container);
2653+
return concatenate(res, reexportContainers);
26142654
}
26152655
const candidates = mapDefined(symbol.declarations, d => !isAmbientModule(d) && d.parent && hasNonGlobalAugmentationExternalModuleSymbol(d.parent) ? getSymbolOfNode(d.parent) : undefined);
26162656
if (!length(candidates)) {
@@ -3981,14 +4021,21 @@ namespace ts {
39814021
/** @param endOfChain Set to false for recursive calls; non-recursive calls should always output something. */
39824022
function getSymbolChain(symbol: Symbol, meaning: SymbolFlags, endOfChain: boolean): Symbol[] | undefined {
39834023
let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & NodeBuilderFlags.UseOnlyExternalAliasing));
3984-
4024+
let parentSpecifiers: (string | undefined)[];
39854025
if (!accessibleSymbolChain ||
39864026
needsQualification(accessibleSymbolChain[0], context.enclosingDeclaration, accessibleSymbolChain.length === 1 ? meaning : getQualifiedLeftMeaning(meaning))) {
39874027

39884028
// Go up and add our parent.
39894029
const parents = getContainersOfSymbol(accessibleSymbolChain ? accessibleSymbolChain[0] : symbol, context.enclosingDeclaration);
39904030
if (length(parents)) {
3991-
for (const parent of parents!) {
4031+
parentSpecifiers = parents!.map(symbol =>
4032+
some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)
4033+
? getSpecifierForModuleSymbol(symbol, context)
4034+
: undefined);
4035+
const indices = parents!.map((_, i) => i);
4036+
indices.sort(sortByBestName);
4037+
const sortedParents = indices.map(i => parents![i]);
4038+
for (const parent of sortedParents) {
39924039
const parentChain = getSymbolChain(parent, getQualifiedLeftMeaning(meaning), /*endOfChain*/ false);
39934040
if (parentChain) {
39944041
accessibleSymbolChain = parentChain.concat(accessibleSymbolChain || [getAliasForSymbolInContainer(parent, symbol) || symbol]);
@@ -4012,6 +4059,25 @@ namespace ts {
40124059
}
40134060
return [symbol];
40144061
}
4062+
4063+
function sortByBestName(a: number, b: number) {
4064+
const specifierA = parentSpecifiers[a];
4065+
const specifierB = parentSpecifiers[b];
4066+
if (specifierA && specifierB) {
4067+
const isBRelative = pathIsRelative(specifierB);
4068+
if (pathIsRelative(specifierA) === isBRelative) {
4069+
// Both relative or both non-relative, sort by number of parts
4070+
return moduleSpecifiers.countPathComponents(specifierA) - moduleSpecifiers.countPathComponents(specifierB);
4071+
}
4072+
if (isBRelative) {
4073+
// A is non-relative, B is relative: prefer A
4074+
return -1;
4075+
}
4076+
// A is relative, B is non-relative: prefer B
4077+
return 1;
4078+
}
4079+
return 0;
4080+
}
40154081
}
40164082
}
40174083

@@ -4115,6 +4181,14 @@ namespace ts {
41154181
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
41164182
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
41174183
const specifier = getSpecifierForModuleSymbol(chain[0], context);
4184+
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && specifier.indexOf("/node_modules/") >= 0) {
4185+
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
4186+
// since declaration files with these kinds of references are liable to fail when published :(
4187+
context.encounteredError = true;
4188+
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
4189+
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
4190+
}
4191+
}
41184192
const lit = createLiteralTypeNode(createLiteral(specifier));
41194193
if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]);
41204194
context.approximateLength += specifier.length + 10; // specifier + import("")

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -2525,6 +2525,10 @@
25252525
"category": "Error",
25262526
"code": 2741
25272527
},
2528+
"The inferred type of '{0}' cannot be named without a reference to '{1}'. This is likely not portable. A type annotation is necessary.": {
2529+
"category": "Error",
2530+
"code": 2742
2531+
},
25282532

25292533
"Import declaration '{0}' is using private name '{1}'.": {
25302534
"category": "Error",

src/compiler/moduleSpecifiers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ namespace ts.moduleSpecifiers {
139139
return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative;
140140
}
141141

142-
function countPathComponents(path: string): number {
142+
export function countPathComponents(path: string): number {
143143
let count = 0;
144144
for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) {
145145
if (path.charCodeAt(i) === CharacterCodes.slash) count++;

src/compiler/transformers/declarations.ts

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ namespace ts {
4545
reportInaccessibleThisError,
4646
reportInaccessibleUniqueSymbolError,
4747
reportPrivateInBaseOfClassExpression,
48+
reportLikelyUnsafeImportRequiredError,
4849
moduleResolverHost: host,
4950
trackReferencedAmbientModule,
5051
trackExternalModuleSymbolOfImportTypeNode
@@ -153,6 +154,14 @@ namespace ts {
153154
}
154155
}
155156

157+
function reportLikelyUnsafeImportRequiredError(specifier: string) {
158+
if (errorNameNode) {
159+
context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_cannot_be_named_without_a_reference_to_1_This_is_likely_not_portable_A_type_annotation_is_necessary,
160+
declarationNameToString(errorNameNode),
161+
specifier));
162+
}
163+
}
164+
156165
function transformRoot(node: Bundle): Bundle;
157166
function transformRoot(node: SourceFile): SourceFile;
158167
function transformRoot(node: SourceFile | Bundle): SourceFile | Bundle;

src/compiler/types.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -3261,15 +3261,17 @@ namespace ts {
32613261
AllowUniqueESSymbolType = 1 << 20,
32623262
AllowEmptyIndexInfoType = 1 << 21,
32633263

3264-
IgnoreErrors = AllowThisInObjectLiteral | AllowQualifedNameInPlaceOfIdentifier | AllowAnonymousIdentifier | AllowEmptyUnionOrIntersection | AllowEmptyTuple | AllowEmptyIndexInfoType,
3264+
// Errors (cont.)
3265+
AllowNodeModulesRelativePaths = 1 << 26,
3266+
/* @internal */ DoNotIncludeSymbolChain = 1 << 27, // Skip looking up and printing an accessible symbol chain
3267+
3268+
IgnoreErrors = AllowThisInObjectLiteral | AllowQualifedNameInPlaceOfIdentifier | AllowAnonymousIdentifier | AllowEmptyUnionOrIntersection | AllowEmptyTuple | AllowEmptyIndexInfoType | AllowNodeModulesRelativePaths,
32653269

32663270
// State
32673271
InObjectTypeLiteral = 1 << 22,
32683272
InTypeAlias = 1 << 23, // Writing type in type alias declaration
32693273
InInitialEntityName = 1 << 24, // Set when writing the LHS of an entity name or entity name expression
32703274
InReverseMappedType = 1 << 25,
3271-
3272-
/* @internal */ DoNotIncludeSymbolChain = 1 << 26, // Skip looking up and printing an accessible symbol chain
32733275
}
32743276

32753277
// Ensure the shared flags between this and `NodeBuilderFlags` stay in alignment
@@ -3650,6 +3652,8 @@ namespace ts {
36503652
originatingImport?: ImportDeclaration | ImportCall; // Import declaration which produced the symbol, present if the symbol is marked as uncallable but had call signatures in `resolveESModuleSymbol`
36513653
lateSymbol?: Symbol; // Late-bound symbol for a computed property
36523654
specifierCache?: Map<string>; // For symbols corresponding to external modules, a cache of incoming path -> module specifier name mappings
3655+
extendedContainers?: Symbol[]; // Containers (other than the parent) which this symbol is aliased in
3656+
extendedContainersByFile?: Map<Symbol[]>; // Containers (other than the parent) which this symbol is aliased in
36533657
variances?: Variance[]; // Alias symbol type argument variance cache
36543658
}
36553659

@@ -5576,6 +5580,7 @@ namespace ts {
55765580
reportInaccessibleThisError?(): void;
55775581
reportPrivateInBaseOfClassExpression?(propertyName: string): void;
55785582
reportInaccessibleUniqueSymbolError?(): void;
5583+
reportLikelyUnsafeImportRequiredError?(specifier: string): void;
55795584
moduleResolverHost?: ModuleSpecifierResolutionHost & { getSourceFiles(): ReadonlyArray<SourceFile>, getCommonSourceDirectory(): string };
55805585
trackReferencedAmbientModule?(decl: ModuleDeclaration, symbol: Symbol): void;
55815586
trackExternalModuleSymbolOfImportTypeNode?(symbol: Symbol): void;

tests/baselines/reference/api/tsserverlibrary.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1976,7 +1976,8 @@ declare namespace ts {
19761976
AllowEmptyTuple = 524288,
19771977
AllowUniqueESSymbolType = 1048576,
19781978
AllowEmptyIndexInfoType = 2097152,
1979-
IgnoreErrors = 3112960,
1979+
AllowNodeModulesRelativePaths = 67108864,
1980+
IgnoreErrors = 70221824,
19801981
InObjectTypeLiteral = 4194304,
19811982
InTypeAlias = 8388608,
19821983
InInitialEntityName = 16777216,

tests/baselines/reference/api/typescript.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1976,7 +1976,8 @@ declare namespace ts {
19761976
AllowEmptyTuple = 524288,
19771977
AllowUniqueESSymbolType = 1048576,
19781978
AllowEmptyIndexInfoType = 2097152,
1979-
IgnoreErrors = 3112960,
1979+
AllowNodeModulesRelativePaths = 67108864,
1980+
IgnoreErrors = 70221824,
19801981
InObjectTypeLiteral = 4194304,
19811982
InTypeAlias = 8388608,
19821983
InInitialEntityName = 16777216,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
tests/cases/compiler/r/entry.ts(3,14): error TS2742: The inferred type of 'x' cannot be named without a reference to 'foo/node_modules/nested'. This is likely not portable. A type annotation is necessary.
2+
3+
4+
==== tests/cases/compiler/r/node_modules/foo/node_modules/nested/index.d.ts (0 errors) ====
5+
export interface NestedProps {}
6+
==== tests/cases/compiler/r/node_modules/foo/other/index.d.ts (0 errors) ====
7+
export interface OtherIndexProps {}
8+
==== tests/cases/compiler/r/node_modules/foo/other.d.ts (0 errors) ====
9+
export interface OtherProps {}
10+
==== tests/cases/compiler/r/node_modules/foo/index.d.ts (0 errors) ====
11+
import { OtherProps } from "./other";
12+
import { OtherIndexProps } from "./other/index";
13+
import { NestedProps } from "nested";
14+
export interface SomeProps {}
15+
16+
export function foo(): [SomeProps, OtherProps, OtherIndexProps, NestedProps];
17+
==== tests/cases/compiler/node_modules/root/index.d.ts (0 errors) ====
18+
export interface RootProps {}
19+
20+
export function bar(): RootProps;
21+
==== tests/cases/compiler/r/entry.ts (1 errors) ====
22+
import { foo } from "foo";
23+
import { bar } from "root";
24+
export const x = foo();
25+
~
26+
!!! error TS2742: The inferred type of 'x' cannot be named without a reference to 'foo/node_modules/nested'. This is likely not portable. A type annotation is necessary.
27+
export const y = bar();
28+

tests/baselines/reference/declarationEmitCommonJsModuleReferencedType.js

-5
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,3 @@ var foo_1 = require("foo");
3131
var root_1 = require("root");
3232
exports.x = foo_1.foo();
3333
exports.y = root_1.bar();
34-
35-
36-
//// [entry.d.ts]
37-
export declare const x: [import("foo").SomeProps, import("foo/other").OtherProps, import("foo/other/index").OtherIndexProps, import("foo/node_modules/nested").NestedProps];
38-
export declare const y: import("root").RootProps;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//// [tests/cases/compiler/declarationEmitReexportedSymlinkReference.ts] ////
2+
3+
//// [index.d.ts]
4+
export * from './types';
5+
//// [types.d.ts]
6+
export declare type A = {
7+
id: string;
8+
};
9+
export declare type B = {
10+
id: number;
11+
};
12+
export declare type IdType = A | B;
13+
export declare class MetadataAccessor<T, D extends IdType = IdType> {
14+
readonly key: string;
15+
private constructor();
16+
toString(): string;
17+
static create<T, D extends IdType = IdType>(key: string): MetadataAccessor<T, D>;
18+
}
19+
//// [package.json]
20+
{
21+
"name": "@raymondfeng/pkg1",
22+
"version": "1.0.0",
23+
"description": "",
24+
"main": "dist/index.js",
25+
"typings": "dist/index.d.ts"
26+
}
27+
//// [index.d.ts]
28+
export * from './types';
29+
//// [types.d.ts]
30+
export * from '@raymondfeng/pkg1';
31+
//// [package.json]
32+
{
33+
"name": "@raymondfeng/pkg2",
34+
"version": "1.0.0",
35+
"description": "",
36+
"main": "dist/index.js",
37+
"typings": "dist/index.d.ts"
38+
}
39+
//// [index.ts]
40+
export * from './keys';
41+
//// [keys.ts]
42+
import {MetadataAccessor} from "@raymondfeng/pkg2";
43+
44+
export const ADMIN = MetadataAccessor.create<boolean>('1');
45+
46+
//// [keys.js]
47+
"use strict";
48+
Object.defineProperty(exports, "__esModule", { value: true });
49+
var pkg2_1 = require("@raymondfeng/pkg2");
50+
exports.ADMIN = pkg2_1.MetadataAccessor.create('1');
51+
//// [index.js]
52+
"use strict";
53+
function __export(m) {
54+
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
55+
}
56+
Object.defineProperty(exports, "__esModule", { value: true });
57+
__export(require("./keys"));
58+
59+
60+
//// [keys.d.ts]
61+
import { MetadataAccessor } from "@raymondfeng/pkg2";
62+
export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
63+
//// [index.d.ts]
64+
export * from './keys';

0 commit comments

Comments
 (0)