[type-summarizer] handle enums, extended interfaces, and export-type'd types (#128200)

This commit is contained in:
Spencer 2022-03-21 14:57:57 -07:00 committed by GitHub
parent c9dfe16725
commit 0682219a8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 491 additions and 23 deletions

View file

@ -41,10 +41,11 @@ export class CollectorResults {
addImportFromNodeModules(
exportInfo: ExportInfo | undefined,
symbol: DecSymbol,
sourceSymbol: DecSymbol,
importSymbol: DecSymbol,
moduleId: string
) {
const imp = ImportedSymbol.fromSymbol(symbol, moduleId);
const imp = ImportedSymbol.fromSymbol(sourceSymbol, importSymbol, moduleId);
imp.exportInfo ||= exportInfo;
this.importedSymbols.add(imp);
}

View file

@ -27,7 +27,8 @@ import { isNodeModule } from '../is_node_module';
interface ResolvedNmImport {
type: 'import_from_node_modules';
symbol: DecSymbol;
importSymbol: DecSymbol;
sourceSymbol: DecSymbol;
moduleId: string;
}
interface ResolvedSymbol {
@ -101,18 +102,31 @@ export class ExportCollector {
const targetPaths = [
...new Set(aliased.declarations.map((d) => this.sourceMapper.getSourceFile(d).fileName)),
];
if (targetPaths.length > 1) {
throw new Error('importing a symbol from multiple locations is unsupported at this time');
let nmCount = 0;
let localCount = 0;
for (const targetPath of targetPaths) {
if (isNodeModule(this.dtsDir, targetPath)) {
nmCount += 1;
} else {
localCount += 1;
}
}
const targetPath = targetPaths[0];
if (isNodeModule(this.dtsDir, targetPath)) {
if (nmCount === targetPaths.length) {
return {
type: 'import_from_node_modules',
symbol,
importSymbol: symbol,
sourceSymbol: aliased,
moduleId: parentImport.moduleSpecifier.text,
};
}
if (localCount === targetPaths.length) {
return undefined;
}
throw new Error('using a symbol which is locally extended is unsupported at this time');
}
}
@ -148,7 +162,12 @@ export class ExportCollector {
const source = this.resolveAliasSymbol(symbol);
if (source.type === 'import_from_node_modules') {
results.addImportFromNodeModules(exportInfo, source.symbol, source.moduleId);
results.addImportFromNodeModules(
exportInfo,
source.sourceSymbol,
source.importSymbol,
source.moduleId
);
return;
}

View file

@ -13,17 +13,17 @@ import { ExportInfo } from '../export_info';
const cache = new WeakMap<DecSymbol, ImportedSymbol>();
export class ImportedSymbol {
static fromSymbol(symbol: DecSymbol, moduleId: string) {
const cached = cache.get(symbol);
static fromSymbol(source: DecSymbol, importSymbol: DecSymbol, moduleId: string) {
const cached = cache.get(source);
if (cached) {
return cached;
}
if (symbol.declarations.length !== 1) {
if (importSymbol.declarations.length !== 1) {
throw new Error('expected import symbol to have exactly one declaration');
}
const dec = symbol.declarations[0];
const dec = importSymbol.declarations[0];
if (
!ts.isImportClause(dec) &&
!ts.isExportSpecifier(dec) &&
@ -41,18 +41,18 @@ export class ImportedSymbol {
}
const imp = ts.isImportClause(dec)
? new ImportedSymbol(symbol, 'default', dec.name.text, dec.isTypeOnly, moduleId)
? new ImportedSymbol(importSymbol, 'default', dec.name.text, dec.isTypeOnly, moduleId)
: ts.isNamespaceImport(dec)
? new ImportedSymbol(symbol, '*', dec.name.text, dec.parent.isTypeOnly, moduleId)
? new ImportedSymbol(importSymbol, '*', dec.name.text, dec.parent.isTypeOnly, moduleId)
: new ImportedSymbol(
symbol,
importSymbol,
dec.name.text,
dec.propertyName?.text,
dec.isTypeOnly || dec.parent.parent.isTypeOnly,
moduleId
);
cache.set(symbol, imp);
cache.set(source, imp);
return imp;
}

View file

@ -124,6 +124,10 @@ export class Printer {
return 'interface';
}
if (node.kind === ts.SyntaxKind.EnumDeclaration) {
return 'enum';
}
if (ts.isVariableDeclaration(node)) {
return this.getVariableDeclarationType(node);
}
@ -131,9 +135,18 @@ export class Printer {
private printModifiers(exportInfo: ExportInfo | undefined, node: ts.Declaration) {
const flags = ts.getCombinedModifierFlags(node);
const keyword = this.getDeclarationKeyword(node);
const modifiers: string[] = [];
if (exportInfo) {
modifiers.push(exportInfo.type);
// always use `export` for explicit types
if (keyword) {
modifiers.push('export');
} else {
modifiers.push(exportInfo.type);
}
}
if ((keyword === 'var' || keyword === 'const') && !exportInfo) {
modifiers.push('declare');
}
if (flags & ts.ModifierFlags.Default) {
modifiers.push('default');
@ -160,7 +173,6 @@ export class Printer {
modifiers.push('async');
}
const keyword = this.getDeclarationKeyword(node);
if (keyword) {
modifiers.push(keyword);
}
@ -292,7 +304,13 @@ export class Printer {
case ts.SyntaxKind.BigIntLiteral:
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.StringKeyword:
return [this.printNode(node)];
case ts.SyntaxKind.TypeReference:
case ts.SyntaxKind.IntersectionType:
return [node.getFullText().trim()];
}
if (ts.isEnumDeclaration(node)) {
return [node.getFullText().trim() + '\n'];
}
if (ts.isFunctionDeclaration(node)) {
@ -335,6 +353,7 @@ export class Printer {
this.printModifiers(exportInfo, node),
this.getMappedSourceNode(node.name),
...(node.type ? [': ', this.printNode(node.type)] : []),
...(node.initializer ? [' = ', this.printNode(node.initializer)] : []),
';\n',
];
}
@ -362,6 +381,9 @@ export class Printer {
this.printModifiers(exportInfo, node),
node.name ? this.getMappedSourceNode(node.name) : [],
this.printTypeParameters(node),
node.heritageClauses
? ` ${node.heritageClauses.map((c) => c.getFullText().trim()).join(' ')}`
: [],
' {\n',
node.members.flatMap((m) => {
const memberText = m.getText();

View file

@ -13,7 +13,8 @@ export type ValueNode =
| ts.FunctionDeclaration
| ts.TypeAliasDeclaration
| ts.VariableDeclaration
| ts.InterfaceDeclaration;
| ts.InterfaceDeclaration
| ts.EnumDeclaration;
export function isExportedValueNode(node: ts.Node): node is ValueNode {
return (
@ -21,7 +22,8 @@ export function isExportedValueNode(node: ts.Node): node is ValueNode {
node.kind === ts.SyntaxKind.FunctionDeclaration ||
node.kind === ts.SyntaxKind.TypeAliasDeclaration ||
node.kind === ts.SyntaxKind.VariableDeclaration ||
node.kind === ts.SyntaxKind.InterfaceDeclaration
node.kind === ts.SyntaxKind.InterfaceDeclaration ||
node.kind === ts.SyntaxKind.EnumDeclaration
);
}
export function assertExportedValueNode(node: ts.Node): asserts node is ValueNode {

View file

@ -152,6 +152,15 @@ class MockCli {
// convert the source files to .d.ts files
this.buildDts();
// copy .d.ts files from source to dist
for (const [rel, content] of Object.entries(this.mockFiles)) {
if (rel.endsWith('.d.ts')) {
const path = Path.resolve(this.dtsOutputDir, rel);
await Fsp.mkdir(Path.dirname(path), { recursive: true });
await Fsp.writeFile(path, dedent(content));
}
}
// summarize the .d.ts files into the output dir
await summarizePackage(log, {
dtsDir: normalizePath(this.dtsOutputDir),
@ -159,7 +168,7 @@ class MockCli {
outputDir: normalizePath(this.outputDir),
repoRelativePackageDir: 'src',
tsconfigPath: normalizePath(this.tsconfigPath),
strictPrinting: false,
strictPrinting: true,
});
// return the results

View file

@ -75,3 +75,64 @@ it('prints basic class correctly', async () => {
"
`);
});
it('prints heritage clauses', async () => {
const output = await run(`
class Foo {
foo() {
return 'foo'
}
}
interface Named {
name: string
}
interface Aged {
age: number
}
export class Bar extends Foo implements Named, Aged {
name = 'bar'
age = 123
bar() {
return this.name
}
}
`);
expect(output.code).toMatchInlineSnapshot(`
"class Foo {
foo(): string;
}
interface Named {
name: string;
}
interface Aged {
age: number;
}
export class Bar extends Foo implements Named, Aged {
name: string;
age: number;
bar(): string;
}
//# sourceMappingURL=index.d.ts.map"
`);
expect(output.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "MAAM,G;EACJ,G;;UAKQ,K;;;UAIA,I;;;aAIG,G;EACX,I;EACA,G;EAEA,G",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [
"index.ts",
],
"version": 3,
}
`);
expect(output.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ]
"
`);
});

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { run } from '../integration_helpers';
it('prints the whole enum, including comments', async () => {
const result = await run(`
/**
* This is an enum
*/
export enum Foo {
/**
* some comment
*/
x,
/**
* some other comment
*/
y,
/**
* some final comment
*/
z = 1,
}
`);
expect(result.code).toMatchInlineSnapshot(`
"/**
* This is an enum
*/
export declare enum Foo {
/**
* some comment
*/
x = 0,
/**
* some other comment
*/
y = 1,
/**
* some final comment
*/
z = 1
}
//# sourceMappingURL=index.d.ts.map"
`);
expect(result.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [],
"version": 3,
}
`);
expect(result.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ]
"
`);
});
it(`handles export-type'd enums`, async () => {
const result = await run(
`
export type { Foo } from './foo'
`,
{
otherFiles: {
['foo.ts']: `
export enum Foo {
x = 1,
y = 2,
z = 3,
}
`,
},
}
);
expect(result.code).toMatchInlineSnapshot(`
"export declare enum Foo {
x = 1,
y = 2,
z = 3
}
//# sourceMappingURL=index.d.ts.map"
`);
expect(result.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [],
"version": 3,
}
`);
expect(result.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [
'packages/kbn-type-summarizer/__tmp__/dist_dts/foo.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts'
]
"
`);
});

View file

@ -123,3 +123,123 @@ it('output links to default import from node modules', async () => {
"
`);
});
it('handles symbols with multiple sources in node_modules', async () => {
const output = await run(
`
export type { Moment } from 'foo';
`,
{
otherFiles: {
['node_modules/foo/index.d.ts']: `
import mo = require('./foo');
export = mo;
`,
['node_modules/foo/foo.d.ts']: `
import mo = require('mo');
export = mo;
declare module "mo" {
export interface Moment {
foo(): string
}
}
`,
['node_modules/mo/index.d.ts']: `
declare namespace mo {
interface Moment extends Object {
add(amount?: number, unit?: number): Moment;
}
}
export = mo;
export as namespace mo;
`,
},
}
);
expect(output.code).toMatchInlineSnapshot(`
"import type { Moment } from 'foo';
export type { Moment };
//# sourceMappingURL=index.d.ts.map"
`);
expect(output.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [],
"version": 3,
}
`);
expect(output.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ]
"
`);
});
it('deduplicates multiple imports to the same type', async () => {
const output = await run(
`
export { Foo1 } from './foo1';
export { Foo2 } from './foo2';
export { Foo3 } from './foo3';
`,
{
otherFiles: {
...nodeModules,
['foo1.ts']: `
import { Foo } from 'foo';
export class Foo1 extends Foo {}
`,
['foo2.ts']: `
import { Foo } from 'foo';
export class Foo2 extends Foo {}
`,
['foo3.ts']: `
import { Foo } from 'foo';
export class Foo3 extends Foo {}
`,
},
}
);
expect(output.code).toMatchInlineSnapshot(`
"import { Foo } from 'foo';
export class Foo1 extends Foo {
}
export class Foo2 extends Foo {
}
export class Foo3 extends Foo {
}
//# sourceMappingURL=index.d.ts.map"
`);
expect(output.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": ";;aACa,I;;aCAA,I;;aCAA,I",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [
"foo1.ts",
"foo2.ts",
"foo3.ts",
],
"version": 3,
}
`);
expect(output.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [
'packages/kbn-type-summarizer/__tmp__/dist_dts/foo1.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/foo2.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/foo3.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts'
]
"
`);
});

View file

@ -60,3 +60,46 @@ it('prints the whole interface, including comments', async () => {
"
`);
});
it(`handles export-type'd interfaces`, async () => {
const result = await run(
`
export type { Foo } from './foo'
`,
{
otherFiles: {
['foo.ts']: `
export interface Foo {
name: string
}
`,
},
}
);
expect(result.code).toMatchInlineSnapshot(`
"export interface Foo {
name: string;
}
//# sourceMappingURL=index.d.ts.map"
`);
expect(result.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "iBAAiB,G",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [
"foo.ts",
],
"version": 3,
}
`);
expect(result.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [
'packages/kbn-type-summarizer/__tmp__/dist_dts/foo.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts'
]
"
`);
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { run } from '../integration_helpers';
it('prints literal number types', async () => {
const output = await run(`
export const NUM = 3;
const NUM2 = 4;
export type PoN = Promise<typeof NUM2>;
`);
expect(output.code).toMatchInlineSnapshot(`
"export const NUM = 3;
declare const NUM2 = 4;
export type PoN = Promise<typeof NUM2>
//# sourceMappingURL=index.d.ts.map"
`);
expect(output.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "aAAa,G;cACP,I;YACM,G",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [
"index.ts",
],
"version": 3,
}
`);
expect(output.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [ 'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts' ]
debug Ignoring 5 global declarations for \\"Promise\\"
"
`);
});

View file

@ -40,3 +40,42 @@ it('prints basic type alias', async () => {
"
`);
});
it(`prints export type'd type alias`, async () => {
const output = await run(
`
export type { Name } from './name'
`,
{
otherFiles: {
['name.ts']: `
export type Name = 'foo';
`,
},
}
);
expect(output.code).toMatchInlineSnapshot(`
"export type Name = 'foo'
//# sourceMappingURL=index.d.ts.map"
`);
expect(output.map).toMatchInlineSnapshot(`
Object {
"file": "index.d.ts",
"mappings": "YAAY,I",
"names": Array [],
"sourceRoot": "../../../src",
"sources": Array [
"name.ts",
],
"version": 3,
}
`);
expect(output.logs).toMatchInlineSnapshot(`
"debug loaded sourcemaps for [
'packages/kbn-type-summarizer/__tmp__/dist_dts/index.d.ts',
'packages/kbn-type-summarizer/__tmp__/dist_dts/name.d.ts'
]
"
`);
});