Skip to content

Commit

Permalink
Add optional type information to rules (palantir#1363)
Browse files Browse the repository at this point in the history
Addresses palantir#1323.

* Changed `Linter` to accept an optional program object in the constructor
  * Added helper functions for creating a program using a tsconfig and getting relevant files
* Created `TypedRule` class which receives the program object
  * Rules extending this class must implement `applyWithProgram`
  * `Linter` passes program to `TypedRule`s if available
  * Calling `apply` on a `TypedRule` throws an error
* Added `requiresTypeInfo` boolean to metadata
* Created `ProgramAwareRuleWalker` which walks with the program / typechecker
* Added cli options `--project` and `--type-check`
  * `--project` takes a `tsconfig.json` file
  * `--type-check` enables type checking and `TypedRule`s, requires `--project`
* Added an example `restrictPlusOperands` rule and tests that uses types
  • Loading branch information
ScottSWu authored and adidahiya committed Jul 11, 2016
1 parent 24017dc commit 9248e05
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 11 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ Options:
-e, --exclude exclude globs from path expansion
-t, --format output format (prose, json, verbose, pmd, msbuild, checkstyle) [default: "prose"]
--test test that tslint produces the correct output for the specified directory
--project path to tsconfig.json file
--type-check enable type checking when linting a project
-v, --version current version
```

Expand Down Expand Up @@ -182,6 +184,14 @@ tslint accepts the following command-line options:
specified directory as the configuration file for the tests. See the
full tslint documentation for more details on how this can be used to test custom rules.
--project:
The location of a tsconfig.json file that will be used to determine which
files will be linted.
--type-check
Enables the type checker when running linting rules. --project must be
specified in order to enable type checking.
-v, --version:
The current version of tslint.
Expand Down Expand Up @@ -214,6 +224,23 @@ const linter = new Linter(fileName, fileContents, options);
const result = linter.lint();
```

#### Type Checking

To enable rules that work with the type checker, a TypeScript program object must be passed to the linter when using the programmatic API. Helper functions are provided to create a program from a `tsconfig.json` file. A project directory can be specified if project files do not lie in the same directory as the `tsconfig.json` file.

```javascript
const program = Linter.createProgram("tsconfig.json", "projectDir/");
const files = Linter.getFileNames(program);
const results = files.map(file => {
const fileContents = program.getSourceFile(file).getFullText();
const linter = new Linter(file, fileContents, options, program);
return result.lint();
});
```

When using the CLI, the `--project` flag will automatically create a program from the specified `tsconfig.json` file. Adding `--type-check` then enables rules that require the type checker.


Core Rules
-----
<sup>[back to ToC &uarr;](#table-of-contents)</sup>
Expand Down Expand Up @@ -313,6 +340,7 @@ Core rules are included in the `tslint` package.
* `"jsx-double"` enforces double quotes for JSX attributes.
* `"avoid-escape"` allows you to use the "other" quotemark in cases where escaping would normally be required. For example, `[true, "double", "avoid-escape"]` would not report a failure on the string literal `'Hello "World"'`.
* `radix` enforces the radix parameter of `parseInt`.
* `restrict-plus-operands` enforces the type of addition operands to be both `string` or both `number` (requires type checking).
* `semicolon` enforces consistent semicolon usage at the end of every statement. Rule options:
* `"always"` enforces semicolons at the end of every statement.
* `"never"` disallows semicolons at the end of every statement except for when they are necessary.
Expand Down
5 changes: 5 additions & 0 deletions src/language/rule/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface IRuleMetadata {
* An explanation of why the rule is useful.
*/
rationale?: string;

/**
* Whether or not the rule requires type info to run.
*/
requiresTypeInfo?: boolean;
}

export type RuleType = "functionality" | "maintainability" | "style" | "typescript";
Expand Down
30 changes: 30 additions & 0 deletions src/language/rule/typedRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license
* Copyright 2016 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as ts from "typescript";

import {AbstractRule} from "./abstractRule";
import {RuleFailure} from "./rule";

export abstract class TypedRule extends AbstractRule {
public apply(sourceFile: ts.SourceFile): RuleFailure[] {
// if no program is given to the linter, throw an error
throw new Error(`${this.getOptions().ruleName} requires type checking`);
}

public abstract applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[];
}
1 change: 1 addition & 0 deletions src/language/walker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

export * from "./blockScopeAwareRuleWalker";
export * from "./programAwareRuleWalker";
export * from "./ruleWalker";
export * from "./scopeAwareRuleWalker";
export * from "./skippableTokenAwareRuleWalker";
Expand Down
39 changes: 39 additions & 0 deletions src/language/walker/programAwareRuleWalker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2016 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as ts from "typescript";

import {IOptions} from "../../lint";
import {RuleWalker} from "./ruleWalker";

export class ProgramAwareRuleWalker extends RuleWalker {
private typeChecker: ts.TypeChecker;

constructor(sourceFile: ts.SourceFile, options: IOptions, private program: ts.Program) {
super(sourceFile, options);

this.typeChecker = program.getTypeChecker();
}

public getProgram(): ts.Program {
return this.program;
}

public getTypeChecker(): ts.TypeChecker {
return this.typeChecker;
}
}
1 change: 1 addition & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
*/

export * from "./language/rule/abstractRule";
export * from "./language/rule/typedRule";
65 changes: 65 additions & 0 deletions src/rules/restrictPlusOperandsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2016 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as ts from "typescript";

import * as Lint from "../lint";

export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "restrict-plus-operands",
description: "When adding two variables, operands must both be of type number or of type string.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "functionality",
requiresTypeInfo: true,
};
/* tslint:enable:object-literal-sort-keys */

public static MISMATCHED_TYPES_FAILURE = "Types of values used in '+' operation must match";
public static UNSUPPORTED_TYPE_FAILURE_FACTORY = (type: string) => `cannot add type ${type}`;

public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithWalker(new RestrictPlusOperandsWalker(sourceFile, this.getOptions(), program));
}
}

class RestrictPlusOperandsWalker extends Lint.ProgramAwareRuleWalker {
public visitBinaryExpression(node: ts.BinaryExpression) {
if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
const tc = this.getTypeChecker();
const leftType = tc.typeToString(tc.getTypeAtLocation(node.left));
const rightType = tc.typeToString(tc.getTypeAtLocation(node.right));

const width = node.getWidth();
const position = node.getStart();

if (leftType !== rightType) {
// mismatched types
this.addFailure(this.createFailure(position, width, Rule.MISMATCHED_TYPES_FAILURE));
} else if (leftType !== "number" && leftType !== "string") {
// adding unsupported types
const failureString = Rule.UNSUPPORTED_TYPE_FAILURE_FACTORY(leftType);
this.addFailure(this.createFailure(position, width, failureString));
}
}

super.visitBinaryExpression(node);
}
}
29 changes: 28 additions & 1 deletion src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import * as diff from "diff";
import * as fs from "fs";
import * as glob from "glob";
import * as path from "path";
import * as ts from "typescript";

import {createCompilerOptions} from "./language/utils";
import {LintError} from "./test/lintError";
import * as parse from "./test/parse";
import * as Linter from "./tslint";
Expand All @@ -46,17 +48,42 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[

for (const fileToLint of filesToLint) {
const fileBasename = path.basename(fileToLint, FILE_EXTENSION);
const fileCompileName = fileBasename.replace(/\.lint$/, "");
const fileText = fs.readFileSync(fileToLint, "utf8");
const fileTextWithoutMarkup = parse.removeErrorMarkup(fileText);
const errorsFromMarkup = parse.parseErrorsFromMarkup(fileText);

const compilerOptions = createCompilerOptions();
const compilerHost: ts.CompilerHost = {
fileExists: () => true,
getCanonicalFileName: (filename: string) => filename,
getCurrentDirectory: () => "",
getDefaultLibFileName: () => ts.getDefaultLibFileName(compilerOptions),
getNewLine: () => "\n",
getSourceFile: function (filenameToGet: string) {
if (filenameToGet === this.getDefaultLibFileName()) {
const fileText = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions)).toString();
return ts.createSourceFile(filenameToGet, fileText, compilerOptions.target);
} else if (filenameToGet === fileCompileName) {
return ts.createSourceFile(fileBasename, fileTextWithoutMarkup, compilerOptions.target, true);
}
},
readFile: () => null,
useCaseSensitiveFileNames: () => true,
writeFile: () => null,
};

const program = ts.createProgram([fileCompileName], compilerOptions, compilerHost);
// perform type checking on the program, updating nodes with symbol table references
ts.getPreEmitDiagnostics(program);

const lintOptions = {
configuration: tslintConfig,
formatter: "prose",
formattersDirectory: "",
rulesDirectory,
};
const linter = new Linter(fileBasename, fileTextWithoutMarkup, lintOptions);
const linter = new Linter(fileBasename, fileTextWithoutMarkup, lintOptions, program);
const errorsFromLinter: LintError[] = linter.lint().failures.map((failure) => {
const startLineAndCharacter = failure.getStartPosition().getLineAndCharacter();
const endLineAndCharacter = failure.getEndPosition().getLineAndCharacter();
Expand Down
3 changes: 3 additions & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@
"language/languageServiceHost.ts",
"language/rule/abstractRule.ts",
"language/rule/rule.ts",
"language/rule/typedRule.ts",
"language/utils.ts",
"language/walker/blockScopeAwareRuleWalker.ts",
"language/walker/index.ts",
"language/walker/programAwareRuleWalker.ts",
"language/walker/ruleWalker.ts",
"language/walker/scopeAwareRuleWalker.ts",
"language/walker/skippableTokenAwareRuleWalker.ts",
Expand Down Expand Up @@ -124,6 +126,7 @@
"rules/orderedImportsRule.ts",
"rules/quotemarkRule.ts",
"rules/radixRule.ts",
"rules/restrictPlusOperandsRule.ts",
"rules/semicolonRule.ts",
"rules/switchDefaultRule.ts",
"rules/trailingCommaRule.ts",
Expand Down
Loading

0 comments on commit 9248e05

Please sign in to comment.