Skip to content

Commit

Permalink
runner.ts: Break into separate functions (palantir#2572)
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-hanson authored and adidahiya committed May 25, 2017
1 parent 3088a2d commit ac0a23a
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 170 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"github": "^8.2.1",
"js-yaml": "^3.7.0",
"json-stringify-pretty-compact": "^1.0.3",
"memory-streams": "^0.1.2",
"mocha": "^3.2.0",
"npm-run-all": "^4.0.2",
"nyc": "^10.2.0",
Expand Down
4 changes: 2 additions & 2 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function findConfiguration(configFile: string | null, inputFilePath: stri
export function findConfigurationPath(suppliedConfigFilePath: string | null, inputFilePath: string) {
if (suppliedConfigFilePath != null) {
if (!fs.existsSync(suppliedConfigFilePath)) {
throw new Error(`Could not find config file at: ${path.resolve(suppliedConfigFilePath)}`);
throw new FatalError(`Could not find config file at: ${path.resolve(suppliedConfigFilePath)}`);
} else {
return path.resolve(suppliedConfigFilePath);
}
Expand Down Expand Up @@ -369,7 +369,7 @@ export function getRulesDirectories(directories?: string | string[], relativeTo?
const absolutePath = getRelativePath(dir, relativeTo);
if (absolutePath != null) {
if (!fs.existsSync(absolutePath)) {
throw new Error(`Could not find custom rule directory: ${dir}`);
throw new FatalError(`Could not find custom rule directory: ${dir}`);
}
}
return absolutePath;
Expand Down
6 changes: 3 additions & 3 deletions src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
loadConfigurationFromPath,
} from "./configuration";
import { removeDisabledFailures } from "./enableDisableRules";
import { isError, showWarningOnce } from "./error";
import { FatalError, isError, showWarningOnce } from "./error";
import { findFormatter } from "./formatterLoader";
import { ILinterOptions, LintResult } from "./index";
import { IFormatter } from "./language/formatter/formatter";
Expand Down Expand Up @@ -220,9 +220,9 @@ class Linter {
const sourceFile = this.program.getSourceFile(fileName);
if (sourceFile === undefined) {
const INVALID_SOURCE_ERROR = dedent`
Invalid source file: ${fileName}. Ensure that the files supplied to lint have a .ts, .tsx, .js or .jsx extension.
Invalid source file: ${fileName}. Ensure that the files supplied to lint have a .ts, .tsx, .d.ts, .js or .jsx extension.
`;
throw new Error(INVALID_SOURCE_ERROR);
throw new FatalError(INVALID_SOURCE_ERROR);
}
return sourceFile;
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/ruleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as fs from "fs";
import * as path from "path";

import { getRelativePath } from "./configuration";
import { showWarningOnce } from "./error";
import { FatalError, showWarningOnce } from "./error";
import { IOptions, IRule, RuleConstructor } from "./language/rule/rule";
import { arrayify, camelize, dedent, find } from "./utils";

Expand Down Expand Up @@ -130,7 +130,7 @@ function loadCachedRule(directory: string, ruleName: string, isCustomPath = fals
if (isCustomPath) {
absolutePath = getRelativePath(directory);
if (absolutePath !== undefined && !fs.existsSync(absolutePath)) {
throw new Error(`Could not find custom rule directory: ${directory}`);
throw new FatalError(`Could not find custom rule directory: ${directory}`);
}
}

Expand Down
271 changes: 139 additions & 132 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
* limitations under the License.
*/

// tslint:disable strict-boolean-expressions prefer-template
// (wait on https://github.com/palantir/tslint/pull/2572)
// tslint:disable strict-boolean-expressions (TODO: Fix up options)

import * as fs from "fs";
import * as glob from "glob";
Expand All @@ -30,10 +29,12 @@ import {
IConfigurationFile,
} from "./configuration";
import { FatalError } from "./error";
import { LintResult } from "./index";
import * as Linter from "./linter";
import { consoleTestResultsHandler, runTests } from "./test";
import { arrayify, flatMap } from "./utils";

export interface IRunnerOptions {
export interface Options {
/**
* Path to a configuration file.
*/
Expand Down Expand Up @@ -105,169 +106,175 @@ export interface IRunnerOptions {
typeCheck?: boolean;
}

export class Runner {
private static trimSingleQuotes(str: string) {
return str.replace(/^'|'$/g, "");
}

constructor(private options: IRunnerOptions, private outputStream: NodeJS.WritableStream) { }
export const enum Status {
Ok = 0,
FatalError = 1,
LintError = 2,
}

public run(onComplete: (status: number) => void) {
if (this.options.init) {
if (fs.existsSync(CONFIG_FILENAME)) {
console.error(`Cannot generate ${CONFIG_FILENAME}: file already exists`);
return onComplete(1);
}
export interface Logger {
log(message: string): void;
error(message: string): void;
}

const tslintJSON = JSON.stringify(DEFAULT_CONFIG, undefined, " ");
fs.writeFileSync(CONFIG_FILENAME, tslintJSON);
return onComplete(0);
export async function run(options: Options, logger: Logger): Promise<Status> {
try {
return await runWorker(options, logger);
} catch (error) {
if ((error as FatalError).name === FatalError.NAME) {
logger.error((error as FatalError).message);
return Status.FatalError;
}
throw error;
}
}

if (this.options.test) {
const results = runTests((this.options.files || []).map(Runner.trimSingleQuotes), this.options.rulesDirectory);
const didAllTestsPass = consoleTestResultsHandler(results);
return onComplete(didAllTestsPass ? 0 : 1);
async function runWorker(options: Options, logger: Logger): Promise<Status> {
if (options.init) {
if (fs.existsSync(CONFIG_FILENAME)) {
throw new FatalError(`Cannot generate ${CONFIG_FILENAME}: file already exists`);
}

// when provided, it should point to an existing location
if (this.options.config && !fs.existsSync(this.options.config)) {
console.error("Invalid option for configuration: " + this.options.config);
return onComplete(1);
}
fs.writeFileSync(CONFIG_FILENAME, JSON.stringify(DEFAULT_CONFIG, undefined, " "));
return Status.Ok;
}

if (options.test) {
const results = runTests((options.files || []).map(trimSingleQuotes), options.rulesDirectory);
return consoleTestResultsHandler(results) ? Status.Ok : Status.FatalError;
}

// if both files and tsconfig are present, use files
let files = this.options.files === undefined ? [] : this.options.files;
let program: ts.Program | undefined;
if (options.config && !fs.existsSync(options.config)) {
throw new FatalError(`Invalid option for configuration: ${options.config}`);
}

if (this.options.project != null) {
const project = findTsconfig(this.options.project);
if (project === undefined) {
console.error("Invalid option for project: " + this.options.project);
return onComplete(1);
}
program = Linter.createProgram(project);
if (files.length === 0) {
files = Linter.getFileNames(program);
}
if (this.options.typeCheck) {
// if type checking, run the type checker
const diagnostics = ts.getPreEmitDiagnostics(program);
if (diagnostics.length > 0) {
const messages = diagnostics.map((diag) => {
// emit any error messages
let message = ts.DiagnosticCategory[diag.category];
if (diag.file) {
const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start!);
let file: string;
const currentDirectory = program!.getCurrentDirectory();
file = this.options.outputAbsolutePaths
? path.resolve(currentDirectory, diag.file.fileName)
: path.relative(currentDirectory, diag.file.fileName);
message += ` at ${file}:${line + 1}:${character + 1}:`;
}
message += " " + ts.flattenDiagnosticMessageText(diag.messageText, "\n");
return message;
});
console.error(messages.join("\n"));
return onComplete(this.options.force ? 0 : 1);
}
const { output, errorCount } = await runLinter(options, logger);
logger.log(output);
return options.force || errorCount === 0 ? Status.Ok : Status.LintError;
}

async function runLinter(options: Options, logger: Logger): Promise<LintResult> {
const { files, program } = resolveFilesAndProgram(options);
// if type checking, run the type checker
if (program && options.typeCheck) {
const diagnostics = ts.getPreEmitDiagnostics(program);
if (diagnostics.length !== 0) {
const message = diagnostics.map((d) => showDiagnostic(d, program, options.outputAbsolutePaths === true)).join("\n");
if (options.force) {
logger.error(message);
} else {
throw new FatalError(message);
}
}
}
return doLinting(options, files, program, logger);
}

let ignorePatterns: string[] = [];
if (this.options.exclude) {
const excludeArguments: string[] = Array.isArray(this.options.exclude) ? this.options.exclude : [this.options.exclude];
function resolveFilesAndProgram({ files, project, exclude, outputAbsolutePaths }: Options): { files: string[]; program?: ts.Program } {
// if both files and tsconfig are present, use files
if (project === undefined) {
return { files: resolveFiles() };
}

ignorePatterns = excludeArguments.map(Runner.trimSingleQuotes);
}
const projectPath = findTsconfig(project);
if (projectPath === undefined) {
throw new FatalError(`Invalid option for project: ${project}`);
}

files = files
// remove single quotes which break matching on Windows when glob is passed in single quotes
.map(Runner.trimSingleQuotes)
.map((file: string) => glob.sync(file, { ignore: ignorePatterns, nodir: true }))
.reduce((a: string[], b: string[]) => a.concat(b), [])
.map((file: string) => {
if (this.options.outputAbsolutePaths) {
return path.resolve(file);
}
return path.relative(process.cwd(), file);
});

try {
this.processFiles(onComplete, files, program);
} catch (error) {
if ((error as FatalError).name === FatalError.NAME) {
console.error((error as FatalError).message);
return onComplete(1);
}
// rethrow unhandled error
throw error;
}
const program = Linter.createProgram(projectPath);
return { files: files === undefined || files.length === 0 ? Linter.getFileNames(program) : resolveFiles(), program };

function resolveFiles(): string[] {
return resolveGlobs(files, exclude, outputAbsolutePaths === true);
}
}

private processFiles(onComplete: (status: number) => void, files: string[], program?: ts.Program) {
const possibleConfigAbsolutePath = this.options.config != null ? path.resolve(this.options.config) : null;
const linter = new Linter({
fix: !!this.options.fix,
formatter: this.options.format,
formattersDirectory: this.options.formattersDirectory || "",
rulesDirectory: this.options.rulesDirectory || "",
}, program);

let lastFolder: string | undefined;
let configFile: IConfigurationFile | undefined;
for (const file of files) {
if (!fs.existsSync(file)) {
console.error(`Unable to open file: ${file}`);
return onComplete(1);
}
function resolveGlobs(files: string[] | undefined, exclude: Options["exclude"], outputAbsolutePaths: boolean): string[] {
const ignore = arrayify(exclude).map(trimSingleQuotes);
return flatMap(arrayify(files), (file) =>
// remove single quotes which break matching on Windows when glob is passed in single quotes
glob.sync(trimSingleQuotes(file), { ignore, nodir: true }))
.map((file) => outputAbsolutePaths ? path.resolve(file) : path.relative(process.cwd(), file));
}

const buffer = new Buffer(256);
const fd = fs.openSync(file, "r");
try {
fs.readSync(fd, buffer, 0, 256, 0);
if (buffer.readInt8(0, true) === 0x47 && buffer.readInt8(188, true) === 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`);
continue;
}
} finally {
fs.closeSync(fd);
}
async function doLinting(
options: Options, files: string[], program: ts.Program | undefined, logger: Logger): Promise<LintResult> {
const possibleConfigAbsolutePath = options.config !== undefined ? path.resolve(options.config) : null;
const linter = new Linter({
fix: !!options.fix,
formatter: options.format,
formattersDirectory: options.formattersDirectory,
rulesDirectory: options.rulesDirectory,
}, program);

let lastFolder: string | undefined;
let configFile: IConfigurationFile | undefined;
for (const file of files) {
if (!fs.existsSync(file)) {
throw new FatalError(`Unable to open file: ${file}`);
}

const contents = fs.readFileSync(file, "utf8");
const contents = await tryReadFile(file, logger);
if (contents !== undefined) {
const folder = path.dirname(file);
if (lastFolder !== folder) {
configFile = findConfiguration(possibleConfigAbsolutePath, folder).results;
lastFolder = folder;
}
linter.lint(file, contents, configFile);
}
}

const lintResult = linter.getResult();
return linter.getResult();
}

this.outputStream.write(lintResult.output, () => {
if (this.options.force || lintResult.errorCount === 0) {
onComplete(0);
} else {
onComplete(2);
}
});
/** Read a file, but return undefined if it is an MPEG '.ts' file. */
async function tryReadFile(filename: string, logger: Logger): Promise<string | undefined> {
const buffer = new Buffer(256);
const fd = fs.openSync(filename, "r");
try {
fs.readSync(fd, buffer, 0, 256, 0);
if (buffer.readInt8(0, true) === 0x47 && buffer.readInt8(188, true) === 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.
logger.error(`${filename}: ignoring MPEG transport stream`);
return undefined;
}
} finally {
fs.closeSync(fd);
}

return fs.readFileSync(filename, "utf8");
}

function showDiagnostic({ file, start, category, messageText }: ts.Diagnostic, program: ts.Program, outputAbsolutePaths: boolean): string {
let message = ts.DiagnosticCategory[category];
if (file) {
const {line, character} = file.getLineAndCharacterOfPosition(start!);
const currentDirectory = program.getCurrentDirectory();
const filePath = outputAbsolutePaths
? path.resolve(currentDirectory, file.fileName)
: path.relative(currentDirectory, file.fileName);
message += ` at ${filePath}:${line + 1}:${character + 1}:`;
}
return `${message} ${ts.flattenDiagnosticMessageText(messageText, "\n")}`;
}

function trimSingleQuotes(str: string): string {
return str.replace(/^'|'$/g, "");
}

function findTsconfig(project: string): string | undefined {
try {
const stats = fs.statSync(project); // throws if file does not exist
if (stats.isDirectory()) {
project = path.join(project, "tsconfig.json");
fs.accessSync(project); // throws if file does not exist
if (!stats.isDirectory()) {
return project;
}
const projectFile = path.join(project, "tsconfig.json");
fs.accessSync(projectFile); // throws if file does not exist
return projectFile;
} catch (e) {
return undefined;
}
return project;
}
Loading

0 comments on commit ac0a23a

Please sign in to comment.