diff --git a/docs/no-import-default-of-export-equals.md b/docs/no-import-default-of-export-equals.md new file mode 100644 index 0000000..b98c24c --- /dev/null +++ b/docs/no-import-default-of-export-equals.md @@ -0,0 +1,22 @@ +# no-import-default-of-export-equals + +Don't use a default import of a package that uses `export =`. +Users who do not have `--allowSyntheticDefaultExports` or `--esModuleInterop` will get different behavior. +This rule only applies to definition files -- for test files you can use a default import if you prefer. + +**Bad**: + +```ts +// foo/index.d.ts +declare interface I {} +export = I; + +// bar/index.d.ts +import I from "foo"; +``` + +**Good**: + +```ts +import I = require("foo"); +``` diff --git a/dtslint.json b/dtslint.json index 34a6e0f..ba638f0 100644 --- a/dtslint.json +++ b/dtslint.json @@ -8,6 +8,7 @@ "no-bad-reference": true, "no-const-enum": true, "no-dead-reference": true, + "no-import-default-of-export-equals": true, "no-padding": true, "no-redundant-undefined": true, "no-relative-import-in-test": true, diff --git a/src/checks.ts b/src/checks.ts index 9025966..f88772c 100644 --- a/src/checks.ts +++ b/src/checks.ts @@ -44,7 +44,6 @@ export async function checkTsconfig(dirPath: string, dt: boolean): Promise module: "commonjs", noEmit: true, forceConsistentCasingInFileNames: true, - esModuleInterop: true, baseUrl, typeRoots: [baseUrl], types: [], @@ -65,6 +64,9 @@ export async function checkTsconfig(dirPath: string, dt: boolean): Promise case "noImplicitThis": case "strictNullChecks": case "strictFunctionTypes": + case "esModuleInterop": + case "allowSyntheticDefaultImports": + // Allow any value break; case "target": case "paths": diff --git a/src/rules/noImportDefaultOfExportEqualsRule.ts b/src/rules/noImportDefaultOfExportEqualsRule.ts new file mode 100644 index 0000000..3d59142 --- /dev/null +++ b/src/rules/noImportDefaultOfExportEqualsRule.ts @@ -0,0 +1,51 @@ +import * as Lint from "tslint"; +import * as ts from "typescript"; + +import { eachModuleStatement, failure, getModuleDeclarationStatements } from "../util"; + +export class Rule extends Lint.Rules.TypedRule { + static metadata: Lint.IRuleMetadata = { + ruleName: "no-import-default-of-export-equals", + description: "Forbid a default import to reference an `export =` module.", + optionsDescription: "Not configurable.", + options: null, + type: "functionality", + typescriptOnly: true, + }; + + static FAILURE_STRING(importName: string, moduleName: string): string { + return failure( + Rule.metadata.ruleName, + `The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`); + } + + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker())); + } +} + +function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { + eachModuleStatement(ctx.sourceFile, statement => { + console.log("!", statement.getText()); + if (!ts.isImportDeclaration(statement)) { + return; + } + const defaultName = statement.importClause && statement.importClause.name; + if (!defaultName) { + return; + } + const sym = checker.getSymbolAtLocation(statement.moduleSpecifier); + if (sym && sym.declarations && sym.declarations.some(d => { + const statements = getStatements(d); + return statements !== undefined && statements.some(ts.isExportAssignment); + })) { + ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText())); + } + }); +} + +function getStatements(decl: ts.Declaration): ReadonlyArray | undefined { + return ts.isSourceFile(decl) ? decl.statements + : ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl) + : undefined; +} diff --git a/src/rules/preferDeclareFunctionRule.ts b/src/rules/preferDeclareFunctionRule.ts index 3e0a551..531bafa 100644 --- a/src/rules/preferDeclareFunctionRule.ts +++ b/src/rules/preferDeclareFunctionRule.ts @@ -1,7 +1,7 @@ import * as Lint from "tslint"; import * as ts from "typescript"; -import { failure } from "../util"; +import { eachModuleStatement, failure } from "../util"; export class Rule extends Lint.Rules.AbstractRule { static metadata: Lint.IRuleMetadata = { @@ -24,7 +24,7 @@ export class Rule extends Lint.Rules.AbstractRule { function walk(ctx: Lint.WalkContext): void { eachModuleStatement(ctx.sourceFile, statement => { - if (isVariableStatement(statement)) { + if (ts.isVariableStatement(statement)) { for (const varDecl of statement.declarationList.declarations) { if (varDecl.type !== undefined && varDecl.type.kind === ts.SyntaxKind.FunctionType) { ctx.addFailureAtNode(varDecl, Rule.FAILURE_STRING); @@ -33,38 +33,3 @@ function walk(ctx: Lint.WalkContext): void { } }); } - -function isVariableStatement(node: ts.Node): node is ts.VariableStatement { - return node.kind === ts.SyntaxKind.VariableStatement; -} - -function eachModuleStatement(sourceFile: ts.SourceFile, action: (statement: ts.Statement) => void): void { - if (!sourceFile.isDeclarationFile) { - return; - } - - for (const node of sourceFile.statements) { - if (isModuleDeclaration(node)) { - let { body } = node; - if (!body) { - return; - } - - while (body.kind === ts.SyntaxKind.ModuleDeclaration) { - body = body.body; - } - - if (body.kind === ts.SyntaxKind.ModuleBlock) { - for (const statement of body.statements) { - action(statement); - } - } - } else { - action(node); - } - } -} - -function isModuleDeclaration(node: ts.Node): node is ts.ModuleDeclaration { - return node.kind === ts.SyntaxKind.ModuleDeclaration; -} diff --git a/src/util.ts b/src/util.ts index dc52fd5..1d1496c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,7 @@ import { readFile } from "fs-promise"; import { basename, dirname } from "path"; import stripJsonComments = require("strip-json-comments"); +import * as ts from "typescript"; export async function readJson(path: string) { const text = await readFile(path, "utf-8"); @@ -23,3 +24,33 @@ export function getCommonDirectoryName(files: ReadonlyArray): string { } return basename(minDir); } + +export function eachModuleStatement(sourceFile: ts.SourceFile, action: (statement: ts.Statement) => void): void { + if (!sourceFile.isDeclarationFile) { + return; + } + + for (const node of sourceFile.statements) { + if (ts.isModuleDeclaration(node)) { + const statements = getModuleDeclarationStatements(node); + if (statements) { + for (const statement of statements) { + action(statement); + } + } + } else { + action(node); + } + } +} + +export function getModuleDeclarationStatements(node: ts.ModuleDeclaration): ReadonlyArray | undefined { + let { body } = node; + if (!body) { + return undefined; + } + while (body.kind === ts.SyntaxKind.ModuleDeclaration) { + body = body.body; + } + return ts.isModuleBlock(body) ? body.statements : undefined; +} diff --git a/test/no-import-default-of-export-equals/test.d.ts.lint b/test/no-import-default-of-export-equals/test.d.ts.lint new file mode 100644 index 0000000..5ffb9e2 --- /dev/null +++ b/test/no-import-default-of-export-equals/test.d.ts.lint @@ -0,0 +1,11 @@ +declare module "a" { + interface I {} + export = I; +} + +declare module "b" { + import a from "a"; + ~ [0] +} + +[0]: The module "a" uses `export = `. Import with `import a = require("a")`. See: https://github.com/Microsoft/dtslint/blob/master/docs/no-import-default-of-export-equals.md diff --git a/test/no-import-default-of-export-equals/tsconfig.json b/test/no-import-default-of-export-equals/tsconfig.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/no-import-default-of-export-equals/tsconfig.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/no-import-default-of-export-equals/tslint.json b/test/no-import-default-of-export-equals/tslint.json new file mode 100644 index 0000000..216d43e --- /dev/null +++ b/test/no-import-default-of-export-equals/tslint.json @@ -0,0 +1,6 @@ +{ + "rulesDirectory": ["../../bin/rules"], + "rules": { + "no-import-default-of-export-equals": true + } +}