Skip to content

Build better import paths for declaration emit/typeToString from reexports if possible #27340

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
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
82 changes: 78 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2599,6 +2599,44 @@ namespace ts {
return getMergedSymbol(symbol.parent && getLateBoundSymbol(symbol.parent));
}

function getAlternativeContainingModules(symbol: Symbol, enclosingDeclaration: Node): Symbol[] {
const containingFile = getSourceFileOfNode(enclosingDeclaration);
const id = "" + getNodeId(containingFile);
const links = getSymbolLinks(symbol);
let results: Symbol[] | undefined;
if (links.extendedContainersByFile && (results = links.extendedContainersByFile.get(id))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This nesting is unnecessarily complicated. What about this:

let results = links.extendedContainersByFile && links.extendedContainersByFile.get(id);
if (results) return results;

return results;
}
if (containingFile && containingFile.imports) {
// Try to make an import using an import already in the enclosing file, if possible
for (const importRef of containingFile.imports) {
if (nodeIsSynthesized(importRef)) continue; // Synthetic names can't be resolved by `resolveExternalModuleName` - they'll cause a debug assert if they error
const resolvedModule = resolveExternalModuleName(enclosingDeclaration, importRef);
if (!resolvedModule) continue;
const ref = getAliasForSymbolInContainer(resolvedModule, symbol);
if (!ref) continue;
results = append(results, resolvedModule);
}
if (length(results)) {
(links.extendedContainersByFile || (links.extendedContainersByFile = createMap())).set(id, results!);
return results!;
}
}
if (links.extendedContainers) {
return links.extendedContainers;
}
// No results from files already being imported by this file - expand search (expensive, but not location-specific, so cached)
const otherFiles = host.getSourceFiles();
for (const file of otherFiles) {
if (!isExternalModule(file)) continue;
const sym = getSymbolOfNode(file);
const ref = getAliasForSymbolInContainer(sym, symbol);
if (!ref) continue;
results = append(results, sym);
}
return links.extendedContainers = results || emptyArray;
}

/**
* Attempts to find the symbol corresponding to the container a symbol is in - usually this
* is just its' `.parent`, but for locals, this value is `undefined`
Expand All @@ -2607,10 +2645,12 @@ namespace ts {
const container = getParentOfSymbol(symbol);
if (container) {
const additionalContainers = mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer);
const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration);
if (enclosingDeclaration && getAccessibleSymbolChain(container, enclosingDeclaration, SymbolFlags.Namespace, /*externalOnly*/ false)) {
return concatenate([container], additionalContainers); // This order expresses a preference for the real container if it is in scope
return concatenate(concatenate([container], additionalContainers), reexportContainers); // This order expresses a preference for the real container if it is in scope
}
return append(additionalContainers, container);
const res = append(additionalContainers, container);
return concatenate(res, reexportContainers);
}
const candidates = mapDefined(symbol.declarations, d => !isAmbientModule(d) && d.parent && hasNonGlobalAugmentationExternalModuleSymbol(d.parent) ? getSymbolOfNode(d.parent) : undefined);
if (!length(candidates)) {
Expand Down Expand Up @@ -3951,14 +3991,21 @@ namespace ts {
/** @param endOfChain Set to false for recursive calls; non-recursive calls should always output something. */
function getSymbolChain(symbol: Symbol, meaning: SymbolFlags, endOfChain: boolean): Symbol[] | undefined {
let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & NodeBuilderFlags.UseOnlyExternalAliasing));

let parentSpecifiers: (string | undefined)[];
if (!accessibleSymbolChain ||
needsQualification(accessibleSymbolChain[0], context.enclosingDeclaration, accessibleSymbolChain.length === 1 ? meaning : getQualifiedLeftMeaning(meaning))) {

// Go up and add our parent.
const parents = getContainersOfSymbol(accessibleSymbolChain ? accessibleSymbolChain[0] : symbol, context.enclosingDeclaration);
if (length(parents)) {
for (const parent of parents!) {
parentSpecifiers = parents!.map(symbol =>
some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)
? getSpecifierForModuleSymbol(symbol, context)
: undefined);
const indices = parents!.map((_, i) => i);
indices.sort(sortByBestName);
const sortedParents = indices.map(i => parents![i]);
for (const parent of sortedParents) {
const parentChain = getSymbolChain(parent, getQualifiedLeftMeaning(meaning), /*endOfChain*/ false);
if (parentChain) {
accessibleSymbolChain = parentChain.concat(accessibleSymbolChain || [getAliasForSymbolInContainer(parent, symbol) || symbol]);
Expand All @@ -3982,6 +4029,25 @@ namespace ts {
}
return [symbol];
}

function sortByBestName(a: number, b: number) {
const specifierA = parentSpecifiers[a];
const specifierB = parentSpecifiers[b];
if (specifierA && specifierB) {
const isBRelative = pathIsRelative(specifierB);
if (pathIsRelative(specifierA) === isBRelative) {
// Both relative or both non-relative, sort by number of parts
return moduleSpecifiers.countPathComponents(specifierA) - moduleSpecifiers.countPathComponents(specifierB);
}
if (isBRelative) {
// A is non-relative, B is relative: prefer A
return -1;
}
// A is relative, B is non-relative: prefer B
return 1;
}
return 0;
}
}
}

Expand Down Expand Up @@ -4085,6 +4151,14 @@ namespace ts {
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
const specifier = getSpecifierForModuleSymbol(chain[0], context);
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && specifier.indexOf("/node_modules/") >= 0) {
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
// since declaration files with these kinds of references are liable to fail when published :(
context.encounteredError = true;
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
}
}
const lit = createLiteralTypeNode(createLiteral(specifier));
if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]);
context.approximateLength += specifier.length + 10; // specifier + import("")
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2525,6 +2525,10 @@
"category": "Error",
"code": 2741
},
"The inferred type of '{0}' cannot be named without a reference to '{1}'. This is likely not portable. A type annotation is necessary.": {
"category": "Error",
"code": 2742
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ namespace ts.moduleSpecifiers {
return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative;
}

function countPathComponents(path: string): number {
export function countPathComponents(path: string): number {
let count = 0;
for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) {
if (path.charCodeAt(i) === CharacterCodes.slash) count++;
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace ts {
reportInaccessibleThisError,
reportInaccessibleUniqueSymbolError,
reportPrivateInBaseOfClassExpression,
reportLikelyUnsafeImportRequiredError,
moduleResolverHost: host,
trackReferencedAmbientModule,
trackExternalModuleSymbolOfImportTypeNode
Expand Down Expand Up @@ -153,6 +154,14 @@ namespace ts {
}
}

function reportLikelyUnsafeImportRequiredError(specifier: string) {
if (errorNameNode) {
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,
declarationNameToString(errorNameNode),
specifier));
}
}

function transformRoot(node: Bundle): Bundle;
function transformRoot(node: SourceFile): SourceFile;
function transformRoot(node: SourceFile | Bundle): SourceFile | Bundle;
Expand Down
11 changes: 8 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3261,15 +3261,17 @@ namespace ts {
AllowUniqueESSymbolType = 1 << 20,
AllowEmptyIndexInfoType = 1 << 21,

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

IgnoreErrors = AllowThisInObjectLiteral | AllowQualifedNameInPlaceOfIdentifier | AllowAnonymousIdentifier | AllowEmptyUnionOrIntersection | AllowEmptyTuple | AllowEmptyIndexInfoType | AllowNodeModulesRelativePaths,

// State
InObjectTypeLiteral = 1 << 22,
InTypeAlias = 1 << 23, // Writing type in type alias declaration
InInitialEntityName = 1 << 24, // Set when writing the LHS of an entity name or entity name expression
InReverseMappedType = 1 << 25,

/* @internal */ DoNotIncludeSymbolChain = 1 << 26, // Skip looking up and printing an accessible symbol chain
}

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

Expand Down Expand Up @@ -5577,6 +5581,7 @@ namespace ts {
reportInaccessibleThisError?(): void;
reportPrivateInBaseOfClassExpression?(propertyName: string): void;
reportInaccessibleUniqueSymbolError?(): void;
reportLikelyUnsafeImportRequiredError?(specifier: string): void;
moduleResolverHost?: ModuleSpecifierResolutionHost & { getSourceFiles(): ReadonlyArray<SourceFile>, getCommonSourceDirectory(): string };
trackReferencedAmbientModule?(decl: ModuleDeclaration, symbol: Symbol): void;
trackExternalModuleSymbolOfImportTypeNode?(symbol: Symbol): void;
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,8 @@ declare namespace ts {
AllowEmptyTuple = 524288,
AllowUniqueESSymbolType = 1048576,
AllowEmptyIndexInfoType = 2097152,
IgnoreErrors = 3112960,
AllowNodeModulesRelativePaths = 67108864,
IgnoreErrors = 70221824,
InObjectTypeLiteral = 4194304,
InTypeAlias = 8388608,
InInitialEntityName = 16777216,
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,8 @@ declare namespace ts {
AllowEmptyTuple = 524288,
AllowUniqueESSymbolType = 1048576,
AllowEmptyIndexInfoType = 2097152,
IgnoreErrors = 3112960,
AllowNodeModulesRelativePaths = 67108864,
IgnoreErrors = 70221824,
InObjectTypeLiteral = 4194304,
InTypeAlias = 8388608,
InInitialEntityName = 16777216,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.


==== tests/cases/compiler/r/node_modules/foo/node_modules/nested/index.d.ts (0 errors) ====
export interface NestedProps {}
==== tests/cases/compiler/r/node_modules/foo/other/index.d.ts (0 errors) ====
export interface OtherIndexProps {}
==== tests/cases/compiler/r/node_modules/foo/other.d.ts (0 errors) ====
export interface OtherProps {}
==== tests/cases/compiler/r/node_modules/foo/index.d.ts (0 errors) ====
import { OtherProps } from "./other";
import { OtherIndexProps } from "./other/index";
import { NestedProps } from "nested";
export interface SomeProps {}

export function foo(): [SomeProps, OtherProps, OtherIndexProps, NestedProps];
==== tests/cases/compiler/node_modules/root/index.d.ts (0 errors) ====
export interface RootProps {}

export function bar(): RootProps;
==== tests/cases/compiler/r/entry.ts (1 errors) ====
import { foo } from "foo";
import { bar } from "root";
export const x = foo();
~
!!! 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.
export const y = bar();

Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,3 @@ var foo_1 = require("foo");
var root_1 = require("root");
exports.x = foo_1.foo();
exports.y = root_1.bar();


//// [entry.d.ts]
export declare const x: [import("foo").SomeProps, import("foo/other").OtherProps, import("foo/other/index").OtherIndexProps, import("foo/node_modules/nested").NestedProps];
export declare const y: import("root").RootProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//// [tests/cases/compiler/declarationEmitReexportedSymlinkReference.ts] ////

//// [index.d.ts]
export * from './types';
//// [types.d.ts]
export declare type A = {
id: string;
};
export declare type B = {
id: number;
};
export declare type IdType = A | B;
export declare class MetadataAccessor<T, D extends IdType = IdType> {
readonly key: string;
private constructor();
toString(): string;
static create<T, D extends IdType = IdType>(key: string): MetadataAccessor<T, D>;
}
//// [package.json]
{
"name": "@raymondfeng/pkg1",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"typings": "dist/index.d.ts"
}
//// [index.d.ts]
export * from './types';
//// [types.d.ts]
export * from '@raymondfeng/pkg1';
//// [package.json]
{
"name": "@raymondfeng/pkg2",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"typings": "dist/index.d.ts"
}
//// [index.ts]
export * from './keys';
//// [keys.ts]
import {MetadataAccessor} from "@raymondfeng/pkg2";

export const ADMIN = MetadataAccessor.create<boolean>('1');

//// [keys.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var pkg2_1 = require("@raymondfeng/pkg2");
exports.ADMIN = pkg2_1.MetadataAccessor.create('1');
//// [index.js]
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./keys"));


//// [keys.d.ts]
import { MetadataAccessor } from "@raymondfeng/pkg2";
export declare const ADMIN: MetadataAccessor<boolean, import("@raymondfeng/pkg2").IdType>;
//// [index.d.ts]
export * from './keys';
Loading