Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

wip: move rules to use eslint #344

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 45 additions & 39 deletions src/rules/dtHeaderRule.ts
Original file line number Diff line number Diff line change
@@ -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>): 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.
}
}
};
157 changes: 67 additions & 90 deletions src/rules/exportJustNamespaceRule.ts
Original file line number Diff line number Diff line change
@@ -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>): 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<TSESTree.TSExportAssignment>();
const namespaces = new Set<string>();
const variables = new Set<string>();

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<ts.Statement>, 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'
});
}
}
}
};
}
};
53 changes: 24 additions & 29 deletions src/rules/noAnyUnionRule.ts
Original file line number Diff line number Diff line change
@@ -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>): 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'
});
}
};
}
};
77 changes: 45 additions & 32 deletions src/rules/noBadReferenceRule.ts
Original file line number Diff line number Diff line change
@@ -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*<reference\s*path=(?:"([^"]*)"|'([^']*)')/;

export class Rule extends Lint.Rules.AbstractRule {
static metadata: Lint.IRuleMetadata = {
ruleName: "no-bad-reference",
description: 'Forbid <reference path="../etc"/> in any file, and forbid <reference path> in test files.',
optionsDescription: "Not configurable.",
options: null,
type: "functionality",
typescriptOnly: true,
};
export const rule: Rule.RuleModule = {
meta: {
docs: {
description: 'Forbid <reference path="../etc"/> in any file, and forbid <reference path> in test files.',
category: 'Functionality'
},
messages: {
noRef: 'Don\'t use <reference path> to reference another package. Use an import or <reference types> instead.',
noRefInTests: 'Don\'t use <reference path> in test files. Use <reference types> or include the file in \'tsconfig.json\'.'
}
},

static FAILURE_STRING = failure(
Rule.metadata.ruleName,
"Don't use <reference path> to reference another package. Use an import or <reference types> instead.");
static FAILURE_STRING_REFERENCE_IN_TEST = failure(
Rule.metadata.ruleName,
"Don't use <reference path> in test files. Use <reference types> 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>): 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);
}
}
}
}
}
};
}
};
Loading