Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit 9248e05

Browse files
ScottSWuadidahiya
authored andcommitted
Add optional type information to rules (#1363)
Addresses #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
1 parent 24017dc commit 9248e05

File tree

19 files changed

+391
-11
lines changed

19 files changed

+391
-11
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ Options:
115115
-e, --exclude exclude globs from path expansion
116116
-t, --format output format (prose, json, verbose, pmd, msbuild, checkstyle) [default: "prose"]
117117
--test test that tslint produces the correct output for the specified directory
118+
--project path to tsconfig.json file
119+
--type-check enable type checking when linting a project
118120
-v, --version current version
119121
```
120122

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

227+
#### Type Checking
228+
229+
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.
230+
231+
```javascript
232+
const program = Linter.createProgram("tsconfig.json", "projectDir/");
233+
const files = Linter.getFileNames(program);
234+
const results = files.map(file => {
235+
const fileContents = program.getSourceFile(file).getFullText();
236+
const linter = new Linter(file, fileContents, options, program);
237+
return result.lint();
238+
});
239+
```
240+
241+
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.
242+
243+
217244
Core Rules
218245
-----
219246
<sup>[back to ToC &uarr;](#table-of-contents)</sup>
@@ -313,6 +340,7 @@ Core rules are included in the `tslint` package.
313340
* `"jsx-double"` enforces double quotes for JSX attributes.
314341
* `"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"'`.
315342
* `radix` enforces the radix parameter of `parseInt`.
343+
* `restrict-plus-operands` enforces the type of addition operands to be both `string` or both `number` (requires type checking).
316344
* `semicolon` enforces consistent semicolon usage at the end of every statement. Rule options:
317345
* `"always"` enforces semicolons at the end of every statement.
318346
* `"never"` disallows semicolons at the end of every statement except for when they are necessary.

src/language/rule/rule.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ export interface IRuleMetadata {
6262
* An explanation of why the rule is useful.
6363
*/
6464
rationale?: string;
65+
66+
/**
67+
* Whether or not the rule requires type info to run.
68+
*/
69+
requiresTypeInfo?: boolean;
6570
}
6671

6772
export type RuleType = "functionality" | "maintainability" | "style" | "typescript";

src/language/rule/typedRule.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright 2016 Palantir Technologies, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as ts from "typescript";
19+
20+
import {AbstractRule} from "./abstractRule";
21+
import {RuleFailure} from "./rule";
22+
23+
export abstract class TypedRule extends AbstractRule {
24+
public apply(sourceFile: ts.SourceFile): RuleFailure[] {
25+
// if no program is given to the linter, throw an error
26+
throw new Error(`${this.getOptions().ruleName} requires type checking`);
27+
}
28+
29+
public abstract applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[];
30+
}

src/language/walker/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
export * from "./blockScopeAwareRuleWalker";
19+
export * from "./programAwareRuleWalker";
1920
export * from "./ruleWalker";
2021
export * from "./scopeAwareRuleWalker";
2122
export * from "./skippableTokenAwareRuleWalker";
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright 2016 Palantir Technologies, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as ts from "typescript";
19+
20+
import {IOptions} from "../../lint";
21+
import {RuleWalker} from "./ruleWalker";
22+
23+
export class ProgramAwareRuleWalker extends RuleWalker {
24+
private typeChecker: ts.TypeChecker;
25+
26+
constructor(sourceFile: ts.SourceFile, options: IOptions, private program: ts.Program) {
27+
super(sourceFile, options);
28+
29+
this.typeChecker = program.getTypeChecker();
30+
}
31+
32+
public getProgram(): ts.Program {
33+
return this.program;
34+
}
35+
36+
public getTypeChecker(): ts.TypeChecker {
37+
return this.typeChecker;
38+
}
39+
}

src/rules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616
*/
1717

1818
export * from "./language/rule/abstractRule";
19+
export * from "./language/rule/typedRule";

src/rules/restrictPlusOperandsRule.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @license
3+
* Copyright 2016 Palantir Technologies, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as ts from "typescript";
19+
20+
import * as Lint from "../lint";
21+
22+
export class Rule extends Lint.Rules.TypedRule {
23+
/* tslint:disable:object-literal-sort-keys */
24+
public static metadata: Lint.IRuleMetadata = {
25+
ruleName: "restrict-plus-operands",
26+
description: "When adding two variables, operands must both be of type number or of type string.",
27+
optionsDescription: "Not configurable.",
28+
options: null,
29+
optionExamples: ["true"],
30+
type: "functionality",
31+
requiresTypeInfo: true,
32+
};
33+
/* tslint:enable:object-literal-sort-keys */
34+
35+
public static MISMATCHED_TYPES_FAILURE = "Types of values used in '+' operation must match";
36+
public static UNSUPPORTED_TYPE_FAILURE_FACTORY = (type: string) => `cannot add type ${type}`;
37+
38+
public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
39+
return this.applyWithWalker(new RestrictPlusOperandsWalker(sourceFile, this.getOptions(), program));
40+
}
41+
}
42+
43+
class RestrictPlusOperandsWalker extends Lint.ProgramAwareRuleWalker {
44+
public visitBinaryExpression(node: ts.BinaryExpression) {
45+
if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
46+
const tc = this.getTypeChecker();
47+
const leftType = tc.typeToString(tc.getTypeAtLocation(node.left));
48+
const rightType = tc.typeToString(tc.getTypeAtLocation(node.right));
49+
50+
const width = node.getWidth();
51+
const position = node.getStart();
52+
53+
if (leftType !== rightType) {
54+
// mismatched types
55+
this.addFailure(this.createFailure(position, width, Rule.MISMATCHED_TYPES_FAILURE));
56+
} else if (leftType !== "number" && leftType !== "string") {
57+
// adding unsupported types
58+
const failureString = Rule.UNSUPPORTED_TYPE_FAILURE_FACTORY(leftType);
59+
this.addFailure(this.createFailure(position, width, failureString));
60+
}
61+
}
62+
63+
super.visitBinaryExpression(node);
64+
}
65+
}

src/test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import * as diff from "diff";
2020
import * as fs from "fs";
2121
import * as glob from "glob";
2222
import * as path from "path";
23+
import * as ts from "typescript";
2324

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

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

56+
const compilerOptions = createCompilerOptions();
57+
const compilerHost: ts.CompilerHost = {
58+
fileExists: () => true,
59+
getCanonicalFileName: (filename: string) => filename,
60+
getCurrentDirectory: () => "",
61+
getDefaultLibFileName: () => ts.getDefaultLibFileName(compilerOptions),
62+
getNewLine: () => "\n",
63+
getSourceFile: function (filenameToGet: string) {
64+
if (filenameToGet === this.getDefaultLibFileName()) {
65+
const fileText = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions)).toString();
66+
return ts.createSourceFile(filenameToGet, fileText, compilerOptions.target);
67+
} else if (filenameToGet === fileCompileName) {
68+
return ts.createSourceFile(fileBasename, fileTextWithoutMarkup, compilerOptions.target, true);
69+
}
70+
},
71+
readFile: () => null,
72+
useCaseSensitiveFileNames: () => true,
73+
writeFile: () => null,
74+
};
75+
76+
const program = ts.createProgram([fileCompileName], compilerOptions, compilerHost);
77+
// perform type checking on the program, updating nodes with symbol table references
78+
ts.getPreEmitDiagnostics(program);
79+
5380
const lintOptions = {
5481
configuration: tslintConfig,
5582
formatter: "prose",
5683
formattersDirectory: "",
5784
rulesDirectory,
5885
};
59-
const linter = new Linter(fileBasename, fileTextWithoutMarkup, lintOptions);
86+
const linter = new Linter(fileBasename, fileTextWithoutMarkup, lintOptions, program);
6087
const errorsFromLinter: LintError[] = linter.lint().failures.map((failure) => {
6188
const startLineAndCharacter = failure.getStartPosition().getLineAndCharacter();
6289
const endLineAndCharacter = failure.getEndPosition().getLineAndCharacter();

src/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@
5959
"language/languageServiceHost.ts",
6060
"language/rule/abstractRule.ts",
6161
"language/rule/rule.ts",
62+
"language/rule/typedRule.ts",
6263
"language/utils.ts",
6364
"language/walker/blockScopeAwareRuleWalker.ts",
6465
"language/walker/index.ts",
66+
"language/walker/programAwareRuleWalker.ts",
6567
"language/walker/ruleWalker.ts",
6668
"language/walker/scopeAwareRuleWalker.ts",
6769
"language/walker/skippableTokenAwareRuleWalker.ts",
@@ -124,6 +126,7 @@
124126
"rules/orderedImportsRule.ts",
125127
"rules/quotemarkRule.ts",
126128
"rules/radixRule.ts",
129+
"rules/restrictPlusOperandsRule.ts",
127130
"rules/semicolonRule.ts",
128131
"rules/switchDefaultRule.ts",
129132
"rules/trailingCommaRule.ts",

0 commit comments

Comments
 (0)