Skip to content

Commit

Permalink
Build better import paths for declaration emit/typeToString from reex…
Browse files Browse the repository at this point in the history
…ports 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
  • Loading branch information
weswigham authored Nov 13, 2018
1 parent 12e371b commit 7a71887
Show file tree
Hide file tree
Showing 29 changed files with 937 additions and 34 deletions.
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))) {
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 @@ -3981,14 +4021,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 @@ -4012,6 +4059,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 @@ -4115,6 +4181,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 @@ -5576,6 +5580,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

0 comments on commit 7a71887

Please sign in to comment.