Skip to content
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

Document and resolve imported types. #646

Closed
wants to merge 8 commits into from
2 changes: 1 addition & 1 deletion src/lib/converter/factories/declaration.ts
Expand Up @@ -52,7 +52,7 @@ export function createDeclaration(context: Context, node: ts.Node, kind: Reflect
}

if (kind === ReflectionKind.ExternalModule) {
isExported = true; // Always mark external modules as exported
isExported = container.isProject(); // Always mark external modules as exported
} else if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclarationList) {
const parentModifiers = ts.getCombinedModifierFlags(node.parent.parent);
isExported = isExported || !!(parentModifiers & ts.ModifierFlags.Export);
Expand Down
90 changes: 90 additions & 0 deletions src/lib/converter/nodes/import.ts
@@ -0,0 +1,90 @@
import * as ts from 'typescript';

import { Reflection, DeclarationReflection } from '../../models/index';
import { createDeclaration } from '../factories/index';
import { Context } from '../context';
import { Component, ConverterNodeComponent } from '../components';

@Component({name: 'node:import'})
export class ImportConverter extends ConverterNodeComponent<ts.ImportDeclaration | ts.ImportEqualsDeclaration> {
/**
* List of supported TypeScript syntax kinds.
*/
supports: ts.SyntaxKind[] = [
ts.SyntaxKind.ImportDeclaration,
ts.SyntaxKind.ImportEqualsDeclaration
];

convert(context: Context, node: ts.ImportDeclaration | ts.ImportEqualsDeclaration): Reflection {
// import * as foo from 'xx'
// import { MyType, MyVar as ImportedVar } from 'xx'
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
const importDeclarationNode = node as ts.ImportDeclaration;
const importClause = importDeclarationNode.importClause;

if (importClause) {
// import myType from 'default-export';
if (importClause.symbol) {
this.createImportedSymbolDeclarationForAlias(context, node, importClause.symbol);
} else if (importClause.namedBindings) {
// import { MyType, MyVar as ImportedVar } from 'xx'
if (importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) {
const namedImports = importClause.namedBindings as ts.NamedImports;
namedImports.elements.forEach(imp => {
if (imp.symbol) {
this.createImportedSymbolDeclarationForAlias(context, node, imp.symbol);
}
});
// import * as foo from 'xx'
} else if (importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
const nsImport = importClause.namedBindings as ts.NamespaceImport;
if (nsImport.symbol) {
this.createImportedSymbolDeclarationForAlias(context, node, nsImport.symbol);
}
}
}
}
// import foo = Foo;
// import bar = require('bar');
} else if (node.kind === ts.SyntaxKind.ImportEqualsDeclaration) {
const importEqualsDeclaration = node as ts.ImportEqualsDeclaration;
const moduleRef = importEqualsDeclaration.moduleReference;
const type = context.checker.getTypeAtLocation(moduleRef);

if (type) {
this.createImportedSymbolDeclaration(context, node, type.symbol, node.symbol.name);
}
}
return context.scope;
}

private createImportedSymbolDeclarationForAlias(context: Context, node: ts.Node, symbol: ts.Symbol): void {
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) === ts.SymbolFlags.Alias) {
this.createImportedSymbolDeclaration(context, node, context.checker.getAliasedSymbol(symbol), symbol.name);
}
}

private createImportedSymbolDeclaration(context: Context, node: ts.Node, symbol: ts.Symbol, name: string): void {
if (symbol && symbol.declarations) {
const project = context.project;
for (let d of symbol.declarations) {
if (!d.symbol) {
continue;
}
const id = project.symbolMapping[context.getSymbolID(d.symbol)];
if (!id) {
continue;
}

const reflection = project.reflections[id];
if (reflection instanceof DeclarationReflection) {
const importedDeclaration = createDeclaration(context, node, reflection.kind, name);
if (importedDeclaration != null) {
importedDeclaration.importedFrom = reflection;
}
break;
}
}
}
}
}
1 change: 1 addition & 0 deletions src/lib/converter/nodes/index.ts
Expand Up @@ -6,6 +6,7 @@ export { ConstructorConverter } from './constructor';
export { EnumConverter } from './enum';
export { ExportConverter } from './export';
export { FunctionConverter } from './function';
export { ImportConverter } from './import';
export { InterfaceConverter } from './interface';
export { TypeLiteralConverter } from './literal-type';
export { ObjectLiteralConverter } from './literal-object';
Expand Down
13 changes: 13 additions & 0 deletions src/lib/models/reflections/declaration.ts
Expand Up @@ -114,6 +114,12 @@ export class DeclarationReflection extends ContainerReflection implements Defaul
*/
implementedBy: Type[];

/**
* If this reflection is an imported reference to another reflection, this is
* the original reflection this reflection points to.
*/
importedFrom?: DeclarationReflection;

/**
* Contains a simplified representation of the type hierarchy suitable for being
* rendered in templates.
Expand Down Expand Up @@ -221,6 +227,13 @@ export class DeclarationReflection extends ContainerReflection implements Defaul
result.implementationOf = this.implementationOf.toObject();
}

if (this.importedFrom) {
result.importedFrom = {
id: this.importedFrom.id,
name: this.importedFrom.name
};
}

return result;
}

Expand Down
24 changes: 16 additions & 8 deletions src/lib/output/themes/DefaultTheme.ts
Expand Up @@ -382,11 +382,15 @@ export class DefaultTheme extends Theme {
* @returns The altered urls array.
*/
static buildUrls(reflection: DeclarationReflection, urls: UrlMapping[]): UrlMapping[] {
const mapping = DefaultTheme.getMapping(reflection);
let urlReflection = reflection;
while (urlReflection.importedFrom) {
urlReflection = urlReflection.importedFrom;
}
const mapping = DefaultTheme.getMapping(urlReflection);
if (mapping) {
if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) {
const url = [mapping.directory, DefaultTheme.getUrl(reflection) + '.html'].join('/');
urls.push(new UrlMapping(url, reflection, mapping.template));
const url = [mapping.directory, DefaultTheme.getUrl(urlReflection) + '.html'].join('/');
urls.push(new UrlMapping(url, urlReflection, mapping.template));

reflection.url = url;
reflection.hasOwnDocument = true;
Expand All @@ -395,13 +399,13 @@ export class DefaultTheme extends Theme {
for (let key in reflection.children) {
const child = reflection.children[key];
if (mapping.isLeaf) {
DefaultTheme.applyAnchorUrl(child, reflection);
DefaultTheme.applyAnchorUrl(child, urlReflection);
} else {
DefaultTheme.buildUrls(child, urls);
}
}
} else {
DefaultTheme.applyAnchorUrl(reflection, reflection.parent);
DefaultTheme.applyAnchorUrl(reflection, urlReflection.parent);
}

return urls;
Expand All @@ -415,14 +419,18 @@ export class DefaultTheme extends Theme {
*/
static applyAnchorUrl(reflection: Reflection, container: Reflection) {
if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) {
let anchor = DefaultTheme.getUrl(reflection, container, '.');
if (reflection['isStatic']) {
let urlReflection = reflection;
while (urlReflection instanceof DeclarationReflection && urlReflection.importedFrom) {
urlReflection = urlReflection.importedFrom;
}
let anchor = DefaultTheme.getUrl(urlReflection, container, '.');
if (urlReflection['isStatic']) {
anchor = 'static-' + anchor;
}

reflection.url = container.url + '#' + anchor;
reflection.anchor = anchor;
reflection.hasOwnDocument = false;
reflection.hasOwnDocument = reflection !== urlReflection;
}

reflection.traverse((child) => {
Expand Down
85 changes: 85 additions & 0 deletions src/test/converter/import/import.ts
@@ -0,0 +1,85 @@
/**
* This is a NamespaceImport.
*/
import * as vars from '../variable/variable';

/**
* This is a NamespaceImport.
*/
import * as enums from '../enum/enum';

/**
* Those are NamedImports.
*/
import { SimpleEnum, ModuleEnum as MyEnum } from '../enum/enum';

/**
* This is an ImportEqualsDeclaration.
*/
import z = require('../enum/enum');

/**
* This is a side-effect-only import and does not create a reflection.
*/
import '../enum/enum';

/**
* This is a NamespaceImport that imports the defaults of a module.
*/
import def from '../export-default/export-default';

/**
* This is a namespace that is aliased later.
*/
namespace Foo
{
/**
* This is a variable of an enum type imported by a namespace import.
*/
let simpleEnum: enums.SimpleEnum = enums.SimpleEnum.EnumValue1;

/**
* This is a variable of an enum type imported by a named import.
*/
export const se = SimpleEnum.EnumValue2;

/**
* This is a variable of an enum type imported by a named import with a custom alias.
*/
let mye: MyEnum = MyEnum.EnumValue3;

/**
* This is a variable of an enum type imported by an ImportEqualsDeclaration.
*/
let zMe: z.ModuleEnum = z.ModuleEnum.EnumValue2;

/**
* This is a const that equals the default export of a module.
*/
const defx = def;

/**
* This is a const that equals a type of an imported module.
*/
const SimpleEnumType = enums.SimpleEnum;

/**
* This is a const that equals an exported variable of an imported module.
*/
const varx = vars.myLet;
}

/**
* This is an ImportEqualsDeclaration to a namespace.
*/
import foo = Foo;

/**
* This is an exported ImportEqualsDeclaration to an exported member of the namespace.
*/
export import fooSe = Foo.se;

/**
* This is a const to test type resolution.
*/
const fooxx = foo.se;