From aa290bbd5114332f4479011f94c18c03dc7d2fe6 Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Sat, 11 May 2019 16:32:00 -0700 Subject: [PATCH 1/7] Improve support for Typescript declare structures --- src/ExportMap.js | 15 ++ tests/files/typescript-declare.d.ts | 33 ++++ tests/files/typescript-export-assign.d.ts | 39 +++++ tests/src/rules/named.js | 186 +++++++++++----------- utils/unambiguous.js | 4 +- 5 files changed, 183 insertions(+), 94 deletions(-) create mode 100644 tests/files/typescript-declare.d.ts create mode 100644 tests/files/typescript-export-assign.d.ts diff --git a/src/ExportMap.js b/src/ExportMap.js index 8513e3d39..584cc2e0f 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -464,6 +464,7 @@ ExportMap.parse = function (path, content, context) { case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser case 'InterfaceDeclaration': + case 'TSDeclareFunction': case 'TSEnumDeclaration': case 'TSTypeAliasDeclaration': case 'TSInterfaceDeclaration': @@ -509,6 +510,20 @@ ExportMap.parse = function (path, content, context) { m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }) }) } + + // This doesn't declare anything, but changes what's being exported. + if (n.type === 'TSExportAssignment') { + const d = ast.body.find( + b => b.type === 'TSModuleDeclaration' && b.id.name === n.expression.name + ) + if (d && d.body && d.body.body) { + d.body.body.forEach(b => { + // Export-assignment exports all members in the namespace, explicitly exported or not. + const s = b.type === 'ExportNamedDeclaration' ? b.declaration : b + m.namespace.set(s.id.name, captureDoc(source, docStyleParsers, b)) + }) + } + } }) return m diff --git a/tests/files/typescript-declare.d.ts b/tests/files/typescript-declare.d.ts new file mode 100644 index 000000000..5d526b85b --- /dev/null +++ b/tests/files/typescript-declare.d.ts @@ -0,0 +1,33 @@ +export declare type MyType = string +export declare enum MyEnum { + Foo, + Bar, + Baz +} +export declare interface Foo { + native: string | number + typedef: MyType + enum: MyEnum +} + +export declare abstract class Bar { + abstract foo(): Foo + + method(); +} + +export declare function getFoo() : MyType; + +export declare module MyModule { + export function ModuleFunction(); +} + +export declare namespace MyNamespace { + export function NamespaceFunction(); + + export module NSModule { + export function NSModuleFunction(); + } +} + +interface NotExported {} diff --git a/tests/files/typescript-export-assign.d.ts b/tests/files/typescript-export-assign.d.ts new file mode 100644 index 000000000..7a3392ee0 --- /dev/null +++ b/tests/files/typescript-export-assign.d.ts @@ -0,0 +1,39 @@ +export = AssignedNamespace; + +declare namespace AssignedNamespace { + type MyType = string + enum MyEnum { + Foo, + Bar, + Baz + } + + interface Foo { + native: string | number + typedef: MyType + enum: MyEnum + } + + abstract class Bar { + abstract foo(): Foo + + method(); + } + + export function getFoo() : MyType; + + export module MyModule { + export function ModuleFunction(); + } + + export namespace MyNamespace { + export function NamespaceFunction(); + + export module NSModule { + export function NSModuleFunction(); + } + } + + // Export-assignment exports all members in the namespace, explicitly exported or not. + // interface NotExported {} +} diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 922914f90..8d8bd41c1 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -292,98 +292,100 @@ context('Typescript', function () { } parsers.forEach((parser) => { - ruleTester.run('named', rule, { - valid: [ - test({ - code: 'import { MyType } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { Foo } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { Bar } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { getFoo } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { MyEnum } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: ` - import { MyModule } from "./typescript" - MyModule.ModuleFunction() - `, - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: ` - import { MyNamespace } from "./typescript" - MyNamespace.NSModule.NSModuleFunction() - `, - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - ], - - invalid: [ - test({ - code: 'import { MissingType } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - errors: [{ - message: "MissingType not found in './typescript'", - type: 'Identifier', - }], - }), - test({ - code: 'import { NotExported } from "./typescript"', - parser: parser, - settings: { - 'import/parsers': { [parser]: ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - errors: [{ - message: "NotExported not found in './typescript'", - type: 'Identifier', - }], - }), - ], + ['typescript', 'typescript-declare', 'typescript-export-assign'].forEach((source) => { + ruleTester.run(`named`, rule, { + valid: [ + test({ + code: `import { MyType } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: `import { Foo } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: `import { Bar } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: `import { getFoo } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: `import { MyEnum } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyModule } from "./${source}" + MyModule.ModuleFunction() + `, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyNamespace } from "./${source}" + MyNamespace.NSModule.NSModuleFunction() + `, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + ], + + invalid: [ + test({ + code: `import { MissingType } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: `MissingType not found in './${source}'`, + type: 'Identifier', + }], + }), + test({ + code: `import { NotExported } from "./${source}"`, + parser: parser, + settings: { + 'import/parsers': { [parser]: ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: `NotExported not found in './${source}'`, + type: 'Identifier', + }], + }), + ], + }) }) }) }) diff --git a/utils/unambiguous.js b/utils/unambiguous.js index a8e842cac..390ad27b6 100644 --- a/utils/unambiguous.js +++ b/utils/unambiguous.js @@ -2,7 +2,7 @@ exports.__esModule = true -const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*]))/m +const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))/m /** * detect possible imports/exports without a full parse. * @@ -18,7 +18,7 @@ exports.test = function isMaybeUnambiguousModule(content) { } // future-/Babel-proof at the expense of being a little loose -const unambiguousNodeType = /^(Exp|Imp)ort.*Declaration$/ +const unambiguousNodeType = /^(((Exp|Imp)ort.*Declaration)|TSExportAssignment)$/ /** * Given an AST, return true if the AST unambiguously represents a module. From 288cedfce0b7fdabcdeb910ff4d4cc1ffe90b385 Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Sun, 12 May 2019 09:39:04 -0700 Subject: [PATCH 2/7] Make groups non-capturing. Co-Authored-By: Jordan Harband --- utils/unambiguous.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/unambiguous.js b/utils/unambiguous.js index 390ad27b6..1dae1d616 100644 --- a/utils/unambiguous.js +++ b/utils/unambiguous.js @@ -18,7 +18,7 @@ exports.test = function isMaybeUnambiguousModule(content) { } // future-/Babel-proof at the expense of being a little loose -const unambiguousNodeType = /^(((Exp|Imp)ort.*Declaration)|TSExportAssignment)$/ +const unambiguousNodeType = /^(?:(?:Exp|Imp)ort.*Declaration|TSExportAssignment)$/ /** * Given an AST, return true if the AST unambiguously represents a module. From 67b1e955f7b17a645d68695d6f1c317cd6100c70 Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Sun, 12 May 2019 10:02:50 -0700 Subject: [PATCH 3/7] Support older typescript parsers --- src/ExportMap.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index 584cc2e0f..c563c94ac 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -464,6 +464,7 @@ ExportMap.parse = function (path, content, context) { case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser case 'InterfaceDeclaration': + case 'DeclareFunction': case 'TSDeclareFunction': case 'TSEnumDeclaration': case 'TSTypeAliasDeclaration': @@ -513,14 +514,20 @@ ExportMap.parse = function (path, content, context) { // This doesn't declare anything, but changes what's being exported. if (n.type === 'TSExportAssignment') { - const d = ast.body.find( - b => b.type === 'TSModuleDeclaration' && b.id.name === n.expression.name + const md = ast.body.find( + (b) => b.type === 'TSModuleDeclaration' && b.id.name === n.expression.name ) - if (d && d.body && d.body.body) { - d.body.body.forEach(b => { + if (md && md.body && md.body.body) { + md.body.body.forEach((b) => { // Export-assignment exports all members in the namespace, explicitly exported or not. const s = b.type === 'ExportNamedDeclaration' ? b.declaration : b - m.namespace.set(s.id.name, captureDoc(source, docStyleParsers, b)) + if (s.type === 'VariableDeclaration') { + s.declarations.forEach((d) => + recursivePatternCapture(d.id, + id => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)))) + } else { + m.namespace.set(s.id.name, captureDoc(source, docStyleParsers, b)) + } }) } } From d1e4455d01d41a386896733413f631c070c37da1 Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Sun, 12 May 2019 19:42:37 -0700 Subject: [PATCH 4/7] Verbose variable names --- src/ExportMap.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index c563c94ac..949df3f19 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -514,19 +514,27 @@ ExportMap.parse = function (path, content, context) { // This doesn't declare anything, but changes what's being exported. if (n.type === 'TSExportAssignment') { - const md = ast.body.find( - (b) => b.type === 'TSModuleDeclaration' && b.id.name === n.expression.name + const moduleDecl = ast.body.find((bodyNode) => + bodyNode.type === 'TSModuleDeclaration' && bodyNode.id.name === n.expression.name ) - if (md && md.body && md.body.body) { - md.body.body.forEach((b) => { + log(moduleDecl) + log(moduleDecl.body) + if (moduleDecl && moduleDecl.body && moduleDecl.body.body) { + moduleDecl.body.body.forEach((moduleBlockNode) => { // Export-assignment exports all members in the namespace, explicitly exported or not. - const s = b.type === 'ExportNamedDeclaration' ? b.declaration : b - if (s.type === 'VariableDeclaration') { - s.declarations.forEach((d) => - recursivePatternCapture(d.id, - id => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)))) + const exportedDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ? + moduleBlockNode.declaration : + moduleBlockNode + + if (exportedDecl.type === 'VariableDeclaration') { + exportedDecl.declarations.forEach((decl) => + recursivePatternCapture(decl.id,(id) => m.namespace.set( + id.name, captureDoc(source, docStyleParsers, decl, exportedDecl, moduleBlockNode)) + ) + ) } else { - m.namespace.set(s.id.name, captureDoc(source, docStyleParsers, b)) + m.namespace.set(exportedDecl.id.name, + captureDoc(source, docStyleParsers, moduleBlockNode)) } }) } From f66e0649601aae5ed16b29b67eb65c2695ad5b2a Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Sun, 12 May 2019 19:43:59 -0700 Subject: [PATCH 5/7] Remove log messages --- src/ExportMap.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index 949df3f19..fb242b564 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -517,8 +517,6 @@ ExportMap.parse = function (path, content, context) { const moduleDecl = ast.body.find((bodyNode) => bodyNode.type === 'TSModuleDeclaration' && bodyNode.id.name === n.expression.name ) - log(moduleDecl) - log(moduleDecl.body) if (moduleDecl && moduleDecl.body && moduleDecl.body.body) { moduleDecl.body.body.forEach((moduleBlockNode) => { // Export-assignment exports all members in the namespace, explicitly exported or not. From 7aa13d14ca0fe890a34f7addadee08606484d68f Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Mon, 13 May 2019 14:56:02 -0700 Subject: [PATCH 6/7] PR feedback Co-Authored-By: Jordan Harband --- src/ExportMap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index fb242b564..dfc315b7d 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -527,7 +527,8 @@ ExportMap.parse = function (path, content, context) { if (exportedDecl.type === 'VariableDeclaration') { exportedDecl.declarations.forEach((decl) => recursivePatternCapture(decl.id,(id) => m.namespace.set( - id.name, captureDoc(source, docStyleParsers, decl, exportedDecl, moduleBlockNode)) + id.name, + captureDoc(source, docStyleParsers, decl, exportedDecl, moduleBlockNode)) ) ) } else { From b52bf3e16bf399c5cf0681c198a3b362e6e7484b Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Mon, 13 May 2019 14:56:12 -0700 Subject: [PATCH 7/7] PR feedback Co-Authored-By: Jordan Harband --- src/ExportMap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index dfc315b7d..d49ab5560 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -532,7 +532,8 @@ ExportMap.parse = function (path, content, context) { ) ) } else { - m.namespace.set(exportedDecl.id.name, + m.namespace.set( + exportedDecl.id.name, captureDoc(source, docStyleParsers, moduleBlockNode)) } })