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

Commit

Permalink
Merge pull request #40 from Microsoft/ts_latest
Browse files Browse the repository at this point in the history
Always use tslint@latest to list; have `expect` rule handle checking with an older TypeScript version
  • Loading branch information
Andy authored May 30, 2017
2 parents 85d5871 + c1267fc commit 038677a
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 51 deletions.
25 changes: 8 additions & 17 deletions src/installer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert = require("assert");
import { exec } from "child_process";
import * as fsp from "fs-promise";
import * as path from "path";
import * as TsLintType from "tslint";
import * as TsType from "typescript";

import { TypeScriptVersion } from "./rules/definitelytyped-header-parser";

Expand All @@ -21,19 +22,15 @@ export async function install(version: TypeScriptVersion | "next"): Promise<void
await fsp.mkdirp(dir);
await fsp.writeJson(path.join(dir, "package.json"), packageJson(version));
await execAndThrowErrors("npm install", dir);
// Copy rules so they use the local typescript/tslint
await fsp.copy(path.join(__dirname, "rules"), path.join(dir, "rules"));
console.log("Installed!");
}
}

export function getLinter(version: TypeScriptVersion | "next"): typeof TsLintType {
const tslintPath = path.join(installDir(version), "node_modules", "tslint");
return require(tslintPath);
}

export function rulesDirectory(version: TypeScriptVersion | "next"): string {
return path.join(installDir(version), "rules");
export function getTypeScript(version: TypeScriptVersion | "next"): typeof TsType {
const tsPath = path.join(installDir(version), "node_modules", "typescript");
const ts = require(tsPath) as typeof TsType;
assert(version === "next" || ts.version.startsWith(version));
return ts;
}

export function cleanInstalls(): Promise<void> {
Expand All @@ -49,7 +46,7 @@ function installDir(version: TypeScriptVersion | "next"): string {
}

/** Run a command and return the stdout, or if there was an error, throw. */
export async function execAndThrowErrors(cmd: string, cwd?: string): Promise<void> {
async function execAndThrowErrors(cmd: string, cwd?: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
exec(cmd, { encoding: "utf8", cwd }, (err, _stdout, stderr) => {
console.error(stderr);
Expand All @@ -62,19 +59,13 @@ export async function execAndThrowErrors(cmd: string, cwd?: string): Promise<voi
});
}

const tslintVersion: string = require("../package.json").dependencies.tslint; // tslint:disable-line:no-var-requires
if (!tslintVersion) {
throw new Error("Missing tslint version.");
}

function packageJson(version: TypeScriptVersion | "next"): {} {
return {
description: `Installs typescript@${version}`,
repository: "N/A",
license: "MIT",
dependencies: {
typescript: version,
tslint: tslintVersion,
},
};
}
25 changes: 15 additions & 10 deletions src/lint.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { exists, readFile } from "fs-promise";
import { join as joinPaths } from "path";
import { Configuration, ILinterOptions } from "tslint";
import { Configuration, ILinterOptions, Linter } from "tslint";
type Configuration = typeof Configuration;
type IConfigurationFile = Configuration.IConfigurationFile;

import { TypeScriptVersion } from "./rules/definitelytyped-header-parser";
import { Options as ExpectOptions } from "./rules/expectRule";

import { getLinter, rulesDirectory } from "./installer";
import { readJson } from "./util";

export async function lintWithVersion(
dirPath: string, version: TypeScriptVersion | "next"): Promise<string | undefined> {
const configPath = getConfigPath(dirPath);
const tslint = getLinter(version);
const program = tslint.Linter.createProgram(joinPaths(dirPath, "tsconfig.json"));
const lintConfigPath = getConfigPath(dirPath);
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
const program = Linter.createProgram(tsconfigPath);

const lintOptions: ILinterOptions = {
fix: false,
formatter: "stylish",
rulesDirectory: rulesDirectory(version),
};
const linter = new tslint.Linter(lintOptions, program);
const config = await getLintConfig(tslint.Configuration, configPath);
const linter = new Linter(lintOptions, program);
const config = await getLintConfig(lintConfigPath, tsconfigPath, version);

for (const filename of program.getRootFileNames()) {
const contents = await readFile(filename, "utf-8");
Expand Down Expand Up @@ -54,12 +53,18 @@ function getConfigPath(dirPath: string): string {
return joinPaths(dirPath, "tslint.json");
}

async function getLintConfig(configuration: Configuration, expectedConfigPath: string): Promise<IConfigurationFile> {
async function getLintConfig(
expectedConfigPath: string,
tsconfigPath: string,
typeScriptVersion: TypeScriptVersion | "next",
): Promise<IConfigurationFile> {
const configPath = await exists(expectedConfigPath) ? expectedConfigPath : joinPaths(__dirname, "..", "dtslint.json");
// Second param to `findConfiguration` doesn't matter, since config path is provided.
const config = configuration.findConfiguration(configPath, "").results;
const config = Configuration.findConfiguration(configPath, "").results;
if (!config) {
throw new Error(`Could not load config at ${configPath}`);
}
const expectOptions: ExpectOptions = { tsconfigPath, typeScriptVersion };
config.rules.get("expect")!.ruleArguments = [expectOptions];
return config;
}
68 changes: 50 additions & 18 deletions src/rules/expectRule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { existsSync, readFileSync } from "fs";
import { dirname, resolve as resolvePath } from "path";
import * as Lint from "tslint";
import * as util from "tsutils";
import * as ts from "typescript";
import * as TsType from "typescript";

import { getTypeScript } from "../installer";
import { TypeScriptVersion } from "./definitelytyped-header-parser";

// Based on https://github.com/danvk/typings-checker

Expand All @@ -25,13 +29,39 @@ export class Rule extends Lint.Rules.TypedRule {
return `Expected type to be:\n ${expectedType}\ngot:\n ${actualType}`;
}

applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program));
applyWithProgram(sourceFile: TsType.SourceFile, program: TsType.Program): Lint.RuleFailure[] {
const options = this.ruleArguments[0] as Options | undefined;
let ts = TsType;
if (options) {
ts = getTypeScript(options.typeScriptVersion);
program = createProgram(options.tsconfigPath, ts, program);
}
return this.applyWithFunction(sourceFile, ctx => walk(ctx, program, ts));
}
}

function walk(ctx: Lint.WalkContext<void>, program: ts.Program): void {
const { sourceFile } = ctx;
export interface Options {
tsconfigPath: string;
typeScriptVersion: TypeScriptVersion | "next";
}

function createProgram(configFile: string, ts: typeof TsType, _oldProgram: TsType.Program): TsType.Program {
const projectDirectory = dirname(configFile);
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
const parseConfigHost: TsType.ParseConfigHost = {
fileExists: existsSync,
readDirectory: ts.sys.readDirectory,
readFile: file => readFileSync(file, "utf8"),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), {noEmit: true});
const host = ts.createCompilerHost(parsed.options, true);
return ts.createProgram(parsed.fileNames, parsed.options, host);
}

function walk(ctx: Lint.WalkContext<void>, program: TsType.Program, ts: typeof TsType): void {
const sourceFile = program.getSourceFile(ctx.sourceFile.fileName);

const checker = program.getTypeChecker();
// Don't care about emit errors.
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
Expand All @@ -44,7 +74,7 @@ function walk(ctx: Lint.WalkContext<void>, program: ts.Program): void {
return;
}

const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile);
const { errorLines, typeAssertions, duplicates } = parseAssertions(sourceFile, ts);

for (const line of duplicates) {
addFailureAtLine(line, Rule.FAILURE_STRING_DUPLICATE_ASSERTION);
Expand All @@ -66,15 +96,15 @@ function walk(ctx: Lint.WalkContext<void>, program: ts.Program): void {
}
}

const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker);
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker, ts);
for (const { node, expected, actual } of unmetExpectations) {
ctx.addFailureAtNode(node, Rule.FAILURE_STRING(expected, actual));
}
for (const line of unusedAssertions) {
addFailureAtLine(line, Rule.FAILURE_STRING_ASSERTION_MISSING_NODE);
}

function addDiagnosticFailure(diagnostic: ts.Diagnostic): void {
function addDiagnosticFailure(diagnostic: TsType.Diagnostic): void {
if (diagnostic.file === sourceFile) {
ctx.addFailureAt(diagnostic.start, diagnostic.length,
"TypeScript compile error: " + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
Expand Down Expand Up @@ -102,7 +132,7 @@ interface Assertions {
duplicates: number[];
}

function parseAssertions(source: ts.SourceFile): Assertions {
function parseAssertions(source: TsType.SourceFile, ts: typeof TsType): Assertions {
const scanner = ts.createScanner(
ts.ScriptTarget.Latest, /*skipTrivia*/false, ts.LanguageVariant.Standard, source.text);
const errorLines = new Set<number>();
Expand Down Expand Up @@ -167,31 +197,33 @@ function parseAssertions(source: ts.SourceFile): Assertions {

interface ExpectTypeFailures {
/** Lines with an $ExpectType, but a different type was there. */
unmetExpectations: Array<{ node: ts.Node, expected: string, actual: string }>;
unmetExpectations: Array<{ node: TsType.Node, expected: string, actual: string }>;
/** Lines with an $ExpectType, but no node could be found. */
unusedAssertions: Iterable<number>;
}

function getExpectTypeFailures(
sourceFile: ts.SourceFile,
sourceFile: TsType.SourceFile,
typeAssertions: Map<number, string>,
checker: ts.TypeChecker,
checker: TsType.TypeChecker,
ts: typeof TsType,
): ExpectTypeFailures {
const unmetExpectations: Array<{ node: ts.Node, expected: string, actual: string }> = [];
const unmetExpectations: Array<{ node: TsType.Node, expected: string, actual: string }> = [];
// Match assertions to the first node that appears on the line they apply to.
ts.forEachChild(sourceFile, iterate);
return { unmetExpectations, unusedAssertions: typeAssertions.keys() };

function iterate(node: ts.Node): void {
function iterate(node: TsType.Node): void {
const line = lineOfPosition(node.getStart(sourceFile), sourceFile);
const expected = typeAssertions.get(line);
if (expected !== undefined) {
// https://github.com/Microsoft/TypeScript/issues/14077
if (util.isExpressionStatement(node)) {
node = node.expression;
if (node.kind === ts.SyntaxKind.ExpressionStatement) {
node = (node as TsType.ExpressionStatement).expression;
}

const type = checker.getTypeAtLocation(node);

const actual = checker.typeToString(type, /*enclosingDeclaration*/ undefined, ts.TypeFormatFlags.NoTruncation);
if (actual !== expected) {
unmetExpectations.push({ node, expected, actual });
Expand All @@ -204,6 +236,6 @@ function getExpectTypeFailures(
}
}

function lineOfPosition(pos: number, sourceFile: ts.SourceFile): number {
function lineOfPosition(pos: number, sourceFile: TsType.SourceFile): number {
return sourceFile.getLineAndCharacterOfPosition(pos).line;
}
1 change: 1 addition & 0 deletions test/expect/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 0 additions & 3 deletions test/expect/tslint.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"rulesDirectory": ["../../bin/rules"],
"linterOptions": {
"typeCheck": true
},
"rules": {
"expect": true
}
Expand Down
1 change: 1 addition & 0 deletions test/no-relative-import-in-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 0 additions & 3 deletions test/no-relative-import-in-test/tslint.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"rulesDirectory": ["../../bin/rules"],
"linterOptions": {
"typeCheck": true
},
"rules": {
"no-relative-import-in-test": true
}
Expand Down

0 comments on commit 038677a

Please sign in to comment.