Skip to content

Commit

Permalink
Call formatter once for all file results (palantir#1472)
Browse files Browse the repository at this point in the history
* Change tslint-cli#processFile to tslint-cli#processFiles

* Extract MultiLinter from Linter

* Always write output stream

* MultiLinter#lint: Make source parameter optional
  • Loading branch information
caugner authored and jkillian committed Sep 9, 2016
1 parent 8db2e4f commit eca8ac7
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 128 deletions.
6 changes: 6 additions & 0 deletions src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,9 @@ export interface ILinterOptions extends ILinterOptionsRaw {
formatter: string | Function;
rulesDirectory: string | string[];
}

export interface IMultiLinterOptions {
formatter?: string | Function;
formattersDirectory?: string;
rulesDirectory?: string | string[];
}
3 changes: 2 additions & 1 deletion src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"test.ts",
"tslint-cli.ts",
"tslint.ts",
"tslintMulti.ts",
"utils.ts",
"configs/latest.ts",
"configs/recommended.ts",
Expand Down Expand Up @@ -98,8 +99,8 @@
"rules/noConditionalAssignmentRule.ts",
"rules/noConsecutiveBlankLinesRule.ts",
"rules/noConsoleRule.ts",
"rules/noConstructorVarsRule.ts",
"rules/noConstructRule.ts",
"rules/noConstructorVarsRule.ts",
"rules/noDebuggerRule.ts",
"rules/noDefaultExportRule.ts",
"rules/noDuplicateKeyRule.ts",
Expand Down
76 changes: 40 additions & 36 deletions src/tslint-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
findConfiguration,
} from "./configuration";
import {consoleTestResultHandler, runTest} from "./test";
import * as Linter from "./tslint";
import * as Linter from "./tslintMulti";

let processed = optimist
.usage("Usage: $0 [options] file ...")
Expand Down Expand Up @@ -217,45 +217,48 @@ if (argv.c && !fs.existsSync(argv.c)) {
}
const possibleConfigAbsolutePath = argv.c != null ? path.resolve(argv.c) : null;

const processFile = (file: string, program?: ts.Program) => {
if (!fs.existsSync(file)) {
console.error(`Unable to open file: ${file}`);
process.exit(1);
}
const processFiles = (files: string[], program?: ts.Program) => {

const buffer = new Buffer(256);
buffer.fill(0);
const fd = fs.openSync(file, "r");
try {
fs.readSync(fd, buffer, 0, 256, null);
if (buffer.readInt8(0) === 0x47 && buffer.readInt8(188) === 0x47) {
// MPEG transport streams use the '.ts' file extension. They use 0x47 as the frame
// separator, repeating every 188 bytes. It is unlikely to find that pattern in
// TypeScript source, so tslint ignores files with the specific pattern.
console.warn(`${file}: ignoring MPEG transport stream`);
return;
const linter = new Linter({
formatter: argv.t,
formattersDirectory: argv.s || "",
rulesDirectory: argv.r || "",
}, program);

for (const file of files) {
if (!fs.existsSync(file)) {
console.error(`Unable to open file: ${file}`);
process.exit(1);
}
} finally {
fs.closeSync(fd);
}

const contents = fs.readFileSync(file, "utf8");
const configuration = findConfiguration(possibleConfigAbsolutePath, file);
const buffer = new Buffer(256);
buffer.fill(0);
const fd = fs.openSync(file, "r");
try {
fs.readSync(fd, buffer, 0, 256, null);
if (buffer.readInt8(0) === 0x47 && buffer.readInt8(188) === 0x47) {
// MPEG transport streams use the '.ts' file extension. They use 0x47 as the frame
// separator, repeating every 188 bytes. It is unlikely to find that pattern in
// TypeScript source, so tslint ignores files with the specific pattern.
console.warn(`${file}: ignoring MPEG transport stream`);
return;
}
} finally {
fs.closeSync(fd);
}

const linter = new Linter(file, contents, {
configuration,
formatter: argv.t,
formattersDirectory: argv.s,
rulesDirectory: argv.r,
}, program);
const contents = fs.readFileSync(file, "utf8");
const configuration = findConfiguration(possibleConfigAbsolutePath, file);
linter.lint(file, contents, configuration);
}

const lintResult = linter.lint();
const lintResult = linter.getResult();

if (lintResult.failureCount > 0) {
outputStream.write(lintResult.output, () => {
outputStream.write(lintResult.output, () => {
if (lintResult.failureCount > 0) {
process.exit(argv.force ? 0 : 2);
});
}
}
});
};

// if both files and tsconfig are present, use files
Expand Down Expand Up @@ -293,6 +296,7 @@ if (argv.project != null) {
}
}

for (const file of files) {
glob.sync(file, { ignore: argv.e }).forEach((file) => processFile(file, program));
}
files = files
.map((file: string) => glob.sync(file, { ignore: argv.e }))
.reduce((a: string[], b: string[]) => a.concat(b));
processFiles(files, program);
102 changes: 12 additions & 90 deletions src/tslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,18 @@ import {
DEFAULT_CONFIG,
findConfiguration,
findConfigurationPath,
getRelativePath,
getRulesDirectories,
loadConfigurationFromPath,
} from "./configuration";
import { EnableDisableRulesWalker } from "./enableDisableRules";
import { findFormatter } from "./formatterLoader";
import { IFormatter } from "./language/formatter/formatter";
import { RuleFailure } from "./language/rule/rule";
import { TypedRule } from "./language/rule/typedRule";
import { getSourceFile } from "./language/utils";
import { ILinterOptions, ILinterOptionsRaw, LintResult } from "./lint";
import { loadRules } from "./ruleLoader";
import * as MultiLinter from "./tslintMulti";
import { arrayify } from "./utils";

/**
* Linter that can lint exactly one file.
*/
class Linter {
public static VERSION = "3.15.1";
public static VERSION = MultiLinter.VERSION;

public static findConfiguration = findConfiguration;
public static findConfigurationPath = findConfigurationPath;
Expand All @@ -49,99 +45,25 @@ class Linter {
* Creates a TypeScript program object from a tsconfig.json file path and optional project directory.
*/
public static createProgram(configFile: string, projectDirectory?: string): ts.Program {
if (projectDirectory === undefined) {
const lastSeparator = configFile.lastIndexOf("/");
if (lastSeparator < 0) {
projectDirectory = ".";
} else {
projectDirectory = configFile.substring(0, lastSeparator + 1);
}
}

const {config} = ts.readConfigFile(configFile, ts.sys.readFile);
const parsed = ts.parseJsonConfigFileContent(config, {readDirectory: ts.sys.readDirectory}, projectDirectory);
const host = ts.createCompilerHost(parsed.options, true);
const program = ts.createProgram(parsed.fileNames, parsed.options, host);

return program;
return MultiLinter.createProgram(configFile, projectDirectory);
}

/**
* Returns a list of source file names from a TypeScript program. This includes all referenced
* files and excludes declaration (".d.ts") files.
*/
public static getFileNames(program: ts.Program): string[] {
return program.getSourceFiles().map(s => s.fileName).filter(l => l.substr(-5) !== ".d.ts");
return MultiLinter.getFileNames(program);
}

constructor(private fileName: string, private source: string, options: ILinterOptionsRaw, private program?: ts.Program) {
this.options = this.computeFullOptions(options);
this.options = this.computeFullOptions(options);
}

public lint(): LintResult {
const failures: RuleFailure[] = [];
let sourceFile: ts.SourceFile;
if (this.program) {
sourceFile = this.program.getSourceFile(this.fileName);
// check if the program has been type checked
if (!("resolvedModules" in sourceFile)) {
throw new Error("Program must be type checked before linting");
}
} else {
sourceFile = getSourceFile(this.fileName, this.source);
}

if (sourceFile === undefined) {
throw new Error(`Invalid source file: ${this.fileName}. Ensure that the files supplied to lint have a .ts or .tsx extension.`);
}

// walk the code first to find all the intervals where rules are disabled
const rulesWalker = new EnableDisableRulesWalker(sourceFile, {
disabledIntervals: [],
ruleName: "",
});
rulesWalker.walk(sourceFile);
const enableDisableRuleMap = rulesWalker.enableDisableRuleMap;

const rulesDirectories = this.options.rulesDirectory;
const configuration = this.options.configuration.rules;
const configuredRules = loadRules(configuration, enableDisableRuleMap, rulesDirectories);
const enabledRules = configuredRules.filter((r) => r.isEnabled());
for (let rule of enabledRules) {
let ruleFailures: RuleFailure[] = [];
if (this.program && rule instanceof TypedRule) {
ruleFailures = rule.applyWithProgram(sourceFile, this.program);
} else {
ruleFailures = rule.apply(sourceFile);
}
for (let ruleFailure of ruleFailures) {
if (!this.containsRule(failures, ruleFailure)) {
failures.push(ruleFailure);
}
}
}

let formatter: IFormatter;
const formattersDirectory = getRelativePath(this.options.formattersDirectory);

const Formatter = findFormatter(this.options.formatter, formattersDirectory);
if (Formatter) {
formatter = new Formatter();
} else {
throw new Error(`formatter '${this.options.formatter}' not found`);
}

const output = formatter.format(failures);
return {
failureCount: failures.length,
failures,
format: this.options.formatter,
output,
};
}

private containsRule(rules: RuleFailure[], rule: RuleFailure) {
return rules.some((r) => r.equals(rule));
const multiLinter: MultiLinter = new MultiLinter(this.options, this.program);
multiLinter.lint(this.fileName, this.source, this.options.configuration);
return multiLinter.getResult();
}

private computeFullOptions(options: ILinterOptionsRaw = {}): ILinterOptions {
Expand All @@ -157,7 +79,7 @@ class Linter {
formattersDirectory,
rulesDirectory: arrayify(rulesDirectory).concat(arrayify(configuration.rulesDirectory)),
};
}
}
}

// tslint:disable-next-line:no-namespace
Expand Down
Loading

0 comments on commit eca8ac7

Please sign in to comment.