diff --git a/src/rules/dtHeaderRule.ts b/src/rules/dtHeaderRule.ts index 82d306e..b228a3c 100644 --- a/src/rules/dtHeaderRule.ts +++ b/src/rules/dtHeaderRule.ts @@ -1,45 +1,51 @@ -import { renderExpected, validate } from "@definitelytyped/header-parser"; -import * as Lint from "tslint"; -import * as ts from "typescript"; -import { failure, isMainFile } from "../util"; +import { isMainFile } from "../util"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "dt-header", - description: "Ensure consistency of DefinitelyTyped headers.", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Ensure consistency of DefinitelyTyped headers.', + category: 'Functionality' + }, + messages: { + headersInMainOnly: 'Header should only be in `index.d.ts` of the root.', + versionInMainOnly: 'TypeScript version should be specified under header in `index.d.ts`.', + authorName: 'Author name should be your name, not the default.' + } + }, - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); + create(context): Rule.RuleListener { + const source = context.getSourceCode(); + const headerTypes = [ + ['Type definitions for', 'headersInMainOnly'], + ['TypeScript Version', 'versionInMainOnly'], + ['Minimum TypeScript Version', 'versionInMainOnly'], + ['Definitions by: My Self', 'authorName'] + ]; + + if (!isMainFile(context.getFilename(), true)) { + return {}; } -} -function walk(ctx: Lint.WalkContext): void { - const { sourceFile } = ctx; - const { text } = sourceFile; - const lookFor = (search: string, explanation: string) => { - const idx = text.indexOf(search); - if (idx !== -1) { - ctx.addFailureAt(idx, search.length, failure(Rule.metadata.ruleName, explanation)); + return { + Program: (node: ESTree.Program): void => { + const comments = source.getAllComments(); + + for (const comment of comments) { + if (comment.type === 'Line') { + const match = headerTypes.find(([prefix]) => + comment.value.startsWith(prefix)); + + if (match) { + context.report({ + node: comment as unknown as ESTree.Node, + messageId: match[1] + }); + } + } } + } }; - if (!isMainFile(sourceFile.fileName, /*allowNested*/ true)) { - lookFor("// Type definitions for", "Header should only be in `index.d.ts` of the root."); - lookFor("// TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`."); - lookFor("// Minimum TypeScript Version", "TypeScript version should be specified under header in `index.d.ts`."); - return; - } - - lookFor("// Definitions by: My Self", "Author name should be your name, not the default."); - const error = validate(text); - if (error) { - ctx.addFailureAt(error.index, 1, failure( - Rule.metadata.ruleName, - `Error parsing header. Expected: ${renderExpected(error.expected)}.`)); - } - // Don't recurse, we're done. -} + } +}; diff --git a/src/rules/exportJustNamespaceRule.ts b/src/rules/exportJustNamespaceRule.ts index a0f464b..fa7df7e 100644 --- a/src/rules/exportJustNamespaceRule.ts +++ b/src/rules/exportJustNamespaceRule.ts @@ -1,96 +1,73 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -import { failure } from "../util"; +type DeclarationLike = + | ESTree.FunctionDeclaration + | ESTree.ClassDeclaration + | TSESTree.TSTypeAliasDeclaration + | TSESTree.TSInterfaceDeclaration + | TSESTree.TSDeclareFunction; +const declarationSelector = [ + 'FunctionDeclaration', + 'ClassDeclaration', + 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', + 'TSDeclareFunction' +].join(','); -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "export-just-namespace", - description: - "Forbid to `export = foo` where `foo` is a namespace and isn't merged with a function/class/type/interface.", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; - - static FAILURE_STRING = failure( - Rule.metadata.ruleName, - "Instead of `export =`-ing a namespace, use the body of the namespace as the module body."); - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbid to `export = foo` where `foo` is a namespace and isn\'t merged with a function/class/type/interface.', + category: 'Functionality' + }, + messages: { + exportNamespace: 'Instead of `export =`-ing a namespace, use the body of the namespace as the module body.' } -} + }, -function walk(ctx: Lint.WalkContext): void { - const { sourceFile: { statements } } = ctx; - const exportEqualsNode = statements.find(isExportEquals) as ts.ExportAssignment | undefined; - if (!exportEqualsNode) { - return; - } - const expr = exportEqualsNode.expression; - if (!ts.isIdentifier(expr)) { - return; - } - const exportEqualsName = expr.text; + create(context): Rule.RuleListener { + const exportNodes = new Set(); + const namespaces = new Set(); + const variables = new Set(); - if (exportEqualsName && isJustNamespace(statements, exportEqualsName)) { - ctx.addFailureAtNode(exportEqualsNode, Rule.FAILURE_STRING); - } -} - -function isExportEquals(node: ts.Node): boolean { - return ts.isExportAssignment(node) && !!node.isExportEquals; -} - -/** Returns true if there is a namespace but there are no functions/classes with the name. */ -function isJustNamespace(statements: ReadonlyArray, exportEqualsName: string): boolean { - let anyNamespace = false; - - for (const statement of statements) { - switch (statement.kind) { - case ts.SyntaxKind.ModuleDeclaration: - anyNamespace = anyNamespace || nameMatches((statement as ts.ModuleDeclaration).name); - break; - case ts.SyntaxKind.VariableStatement: - if ((statement as ts.VariableStatement).declarationList.declarations.some(d => nameMatches(d.name))) { - // OK. It's merged with a variable. - return false; - } - break; - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - if (nameMatches((statement as ts.DeclarationStatement).name)) { - // OK. It's merged with a function/class/type/interface. - return false; - } - break; - default: + return { + TSExportAssignment: (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSExportAssignment; + exportNodes.add(tsNode); + }, + TSModuleDeclaration: (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSModuleDeclaration; + if (tsNode.id.type === 'Identifier') { + namespaces.add(tsNode.id.name); } - } - - return anyNamespace; - - function nameMatches(nameNode: ts.Node | undefined): boolean { - return nameNode !== undefined && ts.isIdentifier(nameNode) && nameNode.text === exportEqualsName; - } -} - -/* -Tests: - -OK: - export = foo; - declare namespace foo {} - declare function foo(): void; // or interface, type, class - -Error: - export = foo; - declare namespace foo {} - -OK: (it's assumed to come from elsewhere) - export = foo; -*/ + }, + VariableDeclaration: (node: ESTree.VariableDeclaration): void => { + for (const decl of node.declarations) { + if (decl.id.type === 'Identifier') { + variables.add(decl.id.name); + } + } + }, + [declarationSelector]: (node: ESTree.Node): void => { + const tsNode = node as unknown as DeclarationLike; + if (tsNode.id.type === 'Identifier') { + variables.add(tsNode.id.name); + } + }, + 'Program:exit': (node: ESTree.Program): void => { + for (const exportNode of exportNodes) { + if (exportNode.expression.type === 'Identifier' && + namespaces.has(exportNode.expression.name) && + !variables.has(exportNode.expression.name)) { + context.report({ + node: exportNode, + messageId: 'exportNamespace' + }); + } + } + } + }; + } +}; diff --git a/src/rules/noAnyUnionRule.ts b/src/rules/noAnyUnionRule.ts index 3807f37..29a60d9 100644 --- a/src/rules/noAnyUnionRule.ts +++ b/src/rules/noAnyUnionRule.ts @@ -1,32 +1,27 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -import { failure } from "../util"; - -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "no-any-union", - description: "Forbid a union to contain `any`", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; - - static FAILURE_STRING = failure( - Rule.metadata.ruleName, - "Including `any` in a union will override all other members of the union."); - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbid a union to contain `any`', + category: 'Functionality' + }, + messages: { + noAny: 'Including `any` in a union will override all other members of the union.' } -} + }, -function walk(ctx: Lint.WalkContext): void { - ctx.sourceFile.forEachChild(function recur(node) { - if (node.kind === ts.SyntaxKind.AnyKeyword && ts.isUnionTypeNode(node.parent!)) { - ctx.addFailureAtNode(node, Rule.FAILURE_STRING); - } - node.forEachChild(recur); - }); -} + create(context): Rule.RuleListener { + return { + 'TSUnionType > TSAnyKeyword': (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSAnyKeyword; + context.report({ + node, + messageId: 'noAny' + }); + } + }; + } +}; diff --git a/src/rules/noBadReferenceRule.ts b/src/rules/noBadReferenceRule.ts index ad3da98..0b1f9b8 100644 --- a/src/rules/noBadReferenceRule.ts +++ b/src/rules/noBadReferenceRule.ts @@ -1,39 +1,52 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -import { failure } from "../util"; +const referencePattern = /^\/\s* in any file, and forbid in test files.', - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbid in any file, and forbid in test files.', + category: 'Functionality' + }, + messages: { + noRef: 'Don\'t use to reference another package. Use an import or instead.', + noRefInTests: 'Don\'t use in test files. Use or include the file in \'tsconfig.json\'.' + } + }, - static FAILURE_STRING = failure( - Rule.metadata.ruleName, - "Don't use to reference another package. Use an import or instead."); - static FAILURE_STRING_REFERENCE_IN_TEST = failure( - Rule.metadata.ruleName, - "Don't use in test files. Use or include the file in 'tsconfig.json'."); + create(context): Rule.RuleListener { + const source = context.getSourceCode(); - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); - } -} + return { + Program: (node: ESTree.Program): void => { + const comments = source.getCommentsBefore(node); + + for (const comment of comments) { + if (comment.type === 'Line') { + const matches = referencePattern.exec(comment.value); + // TODO (43081j): get this from somewhere... + const isDeclarationFile = true; -function walk(ctx: Lint.WalkContext): void { - const { sourceFile } = ctx; - for (const ref of sourceFile.referencedFiles) { - if (sourceFile.isDeclarationFile) { - if (ref.fileName.startsWith("..")) { - ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING); + if (matches) { + if (isDeclarationFile) { + if ((matches[1] || matches[2]).startsWith('..')) { + context.report({ + node: comment as unknown as ESTree.Node, + messageId: 'noRef' + }); + } + } else { + context.report({ + node: comment as unknown as ESTree.Node, + messageId: 'noRefInTests' + }); + } } - } else { - ctx.addFailure(ref.pos, ref.end, Rule.FAILURE_STRING_REFERENCE_IN_TEST); + } } - } -} + } + }; + } +}; diff --git a/src/rules/noConstEnumRule.ts b/src/rules/noConstEnumRule.ts index 1d690ed..cafebdb 100644 --- a/src/rules/noConstEnumRule.ts +++ b/src/rules/noConstEnumRule.ts @@ -1,32 +1,30 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -import { failure } from "../util"; - -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "no-const-enum", - description: "Forbid `const enum`", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; - - static FAILURE_STRING = failure( - Rule.metadata.ruleName, - "Use of `const enum`s is forbidden."); - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbid `const enum`', + category: 'Functionality' + }, + messages: { + noConst: 'Use of `const enum`s is forbidden.' } -} + }, + + create(context): Rule.RuleListener { + return { + TSEnumDeclaration: (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSEnumDeclaration; -function walk(ctx: Lint.WalkContext): void { - ctx.sourceFile.forEachChild(function recur(node) { - if (ts.isEnumDeclaration(node) && node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ConstKeyword)) { - ctx.addFailureAtNode(node.name, Rule.FAILURE_STRING); + if (tsNode.const) { + context.report({ + node, + messageId: 'noConst' + }); } - node.forEachChild(recur); - }); -} + } + }; + } +}; diff --git a/src/rules/noDeadReferenceRule.ts b/src/rules/noDeadReferenceRule.ts index d327893..a3c2c1a 100644 --- a/src/rules/noDeadReferenceRule.ts +++ b/src/rules/noDeadReferenceRule.ts @@ -1,50 +1,36 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; - -import { failure } from "../util"; - -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "no-dead-reference", - description: "Ensures that all `/// ` comments go at the top of the file.", - rationale: "style", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; - - static FAILURE_STRING = failure( - Rule.metadata.ruleName, - "`/// ` directive must be at top of file to take effect."); - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); - } -} - -function walk(ctx: Lint.WalkContext): void { - const { sourceFile: { statements, text } } = ctx; - if (!statements.length) { - return; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; + +const referencePattern = /^\/\s*` comments go at the top of the file.', + category: 'Functionality' + }, + messages: { + noDeadRef: '`/// ` directive must be at top of file to take effect.' } - - // 'm' flag makes it multiline, so `^` matches the beginning of any line. - // 'g' flag lets us set rgx.lastIndex - const rgx = /^\s*(\/\/\/ ` before that is OK.) - rgx.lastIndex = statements[0].getStart(); - - // eslint-disable-next-line no-constant-condition - while (true) { - const match = rgx.exec(text); - if (match === null) { - break; + }, + + create(context): Rule.RuleListener { + const source = context.getSourceCode(); + + return { + Program: (node: ESTree.Program): void => { + const comments = source.getCommentsInside(node); + + for (const comment of comments) { + if (comment.type === 'Line' && referencePattern.test(comment.value)) { + context.report({ + node: comment as unknown as ESTree.Node, + messageId: 'noDeadRef' + }); + } } - - const length = match[1].length; - const start = match.index + match[0].length - length; - ctx.addFailureAt(start, length, Rule.FAILURE_STRING); - } -} + } + }; + } +}; diff --git a/src/rules/noDeclareCurrentPackageRule.ts b/src/rules/noDeclareCurrentPackageRule.ts index a2137ad..60e9171 100644 --- a/src/rules/noDeclareCurrentPackageRule.ts +++ b/src/rules/noDeclareCurrentPackageRule.ts @@ -1,39 +1,47 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; +import { getCommonDirectoryName } from "../util"; -import { failure, getCommonDirectoryName } from "../util"; - -export class Rule extends Lint.Rules.TypedRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "no-declare-current-package", - description: "Don't use an ambient module declaration of the current package; use a normal module.", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: true, - }; +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Don\'t use an ambient module declaration of the current package; use a normal module.', + category: 'Functionality' + }, + messages: { + noDeclare: 'Instead of declaring a module with `declare module "{{text}}"' + + 'write its contents in directly in {{preferred}}.' + } + }, - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - if (!sourceFile.isDeclarationFile) { - return []; - } + create(context): Rule.RuleListener { + const source = context.getSourceCode(); + // TODO (43081j): find this out from somewhere + const isDeclarationFile = true; + // TODO (43081j): compute this path properly + const packageName = getCommonDirectoryName([context.getFilename()]); - const packageName = getCommonDirectoryName(program.getRootFileNames()); - return this.applyWithFunction(sourceFile, ctx => walk(ctx, packageName)); + if (!isDeclarationFile) { + return {}; } -} -function walk(ctx: Lint.WalkContext, packageName: string): void { - for (const statement of ctx.sourceFile.statements) { - if (ts.isModuleDeclaration(statement) && ts.isStringLiteral(statement.name)) { - const { text } = statement.name; - if (text === packageName || text.startsWith(`${packageName}/`)) { - const preferred = text === packageName ? '"index.d.ts"' : `"${text}.d.ts" or "${text}/index.d.ts`; - ctx.addFailureAtNode(statement.name, failure( - Rule.metadata.ruleName, - `Instead of declaring a module with \`declare module "${text}"\`, ` + - `write its contents in directly in ${preferred}.`)); - } + return { + TSModuleDeclaration: (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSModuleDeclaration; + + if (tsNode.id.type === 'Literal' && + typeof tsNode.id.value === 'string' && + (tsNode.id.value === packageName || tsNode.id.value.startsWith(`${packageName}/`))) { + const preferred = tsNode.id.value === packageName ? + '"index.d.ts"' : `"${tsNode.id.value}.d.ts" or "${tsNode.id.value}/index.d.ts`; + context.report({ + node, + data: {preferred, text: tsNode.id.value}, + messageId: 'noDeclare' + }); } - } -} + } + }; + } +}; diff --git a/src/rules/noImportDefaultOfExportEqualsRule.ts b/src/rules/noImportDefaultOfExportEqualsRule.ts index 5f0292e..a733e6f 100644 --- a/src/rules/noImportDefaultOfExportEqualsRule.ts +++ b/src/rules/noImportDefaultOfExportEqualsRule.ts @@ -1,50 +1,68 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import * as ts from 'typescript'; +import { getModuleDeclarationStatements } from "../util"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -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, - }; +// TODO (43081j): does this mean we need typescript as a non-dev +// dependency? or a peer maybe? +function getStatements(decl: ts.Declaration): ReadonlyArray | undefined { + return ts.isSourceFile(decl) ? decl.statements + : ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl) + : undefined; +} - static FAILURE_STRING(importName: string, moduleName: string): string { - return failure( - Rule.metadata.ruleName, - `The module ${moduleName} uses \`export = \`. Import with \`import ${importName} = require(${moduleName})\`.`); +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbid a default import to reference an `export =` module.', + category: 'Functionality' + }, + messages: { + noDefaultImport: '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())); + create(context): Rule.RuleListener { + if (!context.parserServices || + !context.parserServices.program || + !context.parserServices.hasFullTypeInformation) { + return {}; } -} -function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { - eachModuleStatement(ctx.sourceFile, statement => { - 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(s => ts.isExportAssignment(s) && !!s.isExportEquals); - })) { - ctx.addFailureAtNode(defaultName, Rule.FAILURE_STRING(defaultName.text, statement.moduleSpecifier.getText())); + const checker = context.parserServices.program.getTypeChecker(); + const hasExportAssignment = (declaration: ts.Declaration): boolean => { + const statements = getStatements(declaration); + return statements !== undefined && + statements.some(s => ts.isExportAssignment(s) && !!s.isExportEquals); + }; + + return { + ImportDeclaration: (node: ESTree.ImportDeclaration): void => { + if (node.source.type !== 'Literal' || + typeof node.source.value !== 'string') { + return; } - }); -} -function getStatements(decl: ts.Declaration): ReadonlyArray | undefined { - return ts.isSourceFile(decl) ? decl.statements - : ts.isModuleDeclaration(decl) ? getModuleDeclarationStatements(decl) - : undefined; -} + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportDefaultSpecifier') { + // TODO (43081j): figure out what we should be getting the + // symbol of + const sym = checker.getSymbolAtLocation(specifier.local); + if (sym?.declarations && sym.declarations.some(hasExportAssignment)) { + context.report({ + node, + messageId: 'noDefaultImport', + data: { + moduleName: node.source.value, + importName: specifier.local.name + } + }); + } + } + } + } + }; + } +}; diff --git a/src/rules/noRelativeImportInTestRule.ts b/src/rules/noRelativeImportInTestRule.ts index 712a2d3..d9fd266 100644 --- a/src/rules/noRelativeImportInTestRule.ts +++ b/src/rules/noRelativeImportInTestRule.ts @@ -1,52 +1,54 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; - -import { failure } from "../util"; - -export class Rule extends Lint.Rules.TypedRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "no-relative-import-in-test", - description: "Forbids test (non-declaration) files to use relative imports.", - optionsDescription: "Not configurable.", - options: null, - type: "functionality", - typescriptOnly: false, - }; - - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - if (sourceFile.isDeclarationFile) { - return []; - } +import {Rule} from 'eslint'; +import * as ts from 'typescript'; +import * as ESTree from 'estree'; + +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbids test (non-declaration) files to use relative imports.', + category: 'Functionality' + }, + messages: { + noRelativeImport: 'Test file should not use a relative import. ' + + 'Use a global import as if this were a user of the package.' + } + }, - return this.applyWithFunction(sourceFile, ctx => walk(ctx, program.getTypeChecker())); + create(context): Rule.RuleListener { + if (!context.parserServices || + !context.parserServices.program || + !context.parserServices.hasFullTypeInformation) { + return {}; } -} -const FAILURE_STRING = failure( - Rule.metadata.ruleName, - "Test file should not use a relative import. Use a global import as if this were a user of the package."); + // TODO (43081j): how do we get this? + const isDeclarationFile = false; -function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { - const { sourceFile } = ctx; + if (isDeclarationFile) { + return {}; + } - for (const i of sourceFile.imports) { - if (i.text.startsWith(".")) { - const moduleSymbol = checker.getSymbolAtLocation(i); - if (!moduleSymbol || !moduleSymbol.declarations) { - continue; - } + const checker = context.parserServices.program.getTypeChecker(); + return { + ImportDeclaration: (node: ESTree.ImportDeclaration): void => { + if (typeof node.source.value === 'string' && + node.source.value.startsWith('.')) { + // TODO (43081j): check this is actually resolving the right thing + const moduleSymbol = checker.getSymbolAtLocation(node.source); + if (moduleSymbol && + moduleSymbol.declarations) { for (const decl of moduleSymbol.declarations) { - if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) { - ctx.addFailureAtNode(i, FAILURE_STRING); - } + if (decl.kind === ts.SyntaxKind.SourceFile && (decl as ts.SourceFile).isDeclarationFile) { + context.report({ + node, + messageId: 'noRelativeImport' + }); + } } + } } - } -} - -declare module "typescript" { - interface SourceFile { - imports: ReadonlyArray; - } -} + } + }; + } +}; diff --git a/src/rules/redundantUndefinedRule.ts b/src/rules/redundantUndefinedRule.ts index 22752a2..94f4bcf 100644 --- a/src/rules/redundantUndefinedRule.ts +++ b/src/rules/redundantUndefinedRule.ts @@ -1,40 +1,34 @@ -import * as Lint from "tslint"; -import * as ts from "typescript"; +import {Rule} from 'eslint'; +import * as ESTree from 'estree'; +import {TSESTree} from '@typescript-eslint/experimental-utils'; -import { failure } from "../util"; +const selector = 'TSPropertySignature[optional] TSUnionType > TSUndefinedKeyword,' + + 'ClassProperty[optional] TSUnionType > TSUndefinedKeyword'; -export class Rule extends Lint.Rules.AbstractRule { - static metadata: Lint.IRuleMetadata = { - ruleName: "redundant-undefined", - description: "Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.", - optionsDescription: "Not configurable.", - options: null, - type: "style", - typescriptOnly: true, - }; - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithFunction(sourceFile, walk); +export const rule: Rule.RuleModule = { + meta: { + docs: { + description: 'Forbids optional parameters to include an explicit `undefined` in their type; requires it in optional properties.', + category: 'Style' + }, + messages: { + noUndefined: 'Parameter is optional, so no need to include `undefined` in the type.' } -} + }, -function walk(ctx: Lint.WalkContext): void { - if (ctx.sourceFile.fileName.includes('node_modules')) return; - ctx.sourceFile.forEachChild(function recur(node) { - if (node.kind === ts.SyntaxKind.UndefinedKeyword - && ts.isUnionTypeNode(node.parent!) - && isOptionalParameter(node.parent!.parent!)) { - ctx.addFailureAtNode( - node, - failure( - Rule.metadata.ruleName, - `Parameter is optional, so no need to include \`undefined\` in the type.`)); - } - node.forEachChild(recur); - }); -} + create(context): Rule.RuleListener { + if (context.getFilename().includes('node_modules')) { + return {}; + } + return { + [selector]: (node: ESTree.Node): void => { + const tsNode = node as unknown as TSESTree.TSUndefinedKeyword; -function isOptionalParameter(node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.Parameter - && (node as ts.ParameterDeclaration).questionToken !== undefined; -} + context.report({ + node, + messageId: 'noUndefined' + }); + } + }; + } +};