From 8f93bfb650039e8d3e3e06505157acd277c50798 Mon Sep 17 00:00:00 2001 From: w0rp Date: Fri, 6 Apr 2018 10:57:07 +0100 Subject: [PATCH 1/2] Experiment with --stdin and --stdin-filename support for tslint --- src/runner.ts | 105 +++++++++++++++++++++++++++++++++++++++-------- src/tslintCli.ts | 40 +++++++++++++++++- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/src/runner.ts b/src/runner.ts index 3b3c80d2f55..cbfc073e234 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -94,6 +94,16 @@ export interface Options { */ rulesDirectory?: string | string[]; + /** + * Check files via stdin. + */ + stdin?: boolean; + + /** + * Tell TSLint where the actual file being linted through stdin is, if any. + */ + stdinFilename?: string; + /** * Run the tests in the given directories to ensure a (custom) TSLint rule's output matches the expected output. * When this property is `true` the `files` property is used to specify the directories from which the tests should be executed. @@ -157,7 +167,9 @@ async function runWorker(options: Options, logger: Logger): Promise { } async function runLinter(options: Options, logger: Logger): Promise { - const { files, program } = resolveFilesAndProgram(options, logger); + const program = resolveProgram(options); + const files = resolveFiles(options, program, logger); + // if type checking, run the type checker if (program && options.typeCheck) { const diagnostics = ts.getPreEmitDiagnostics(program); @@ -173,25 +185,80 @@ async function runLinter(options: Options, logger: Logger): Promise return doLinting(options, files, program, logger); } -function resolveFilesAndProgram( - { files, project, exclude, outputAbsolutePaths }: Options, +/** Read the stdin stream into a string with all of the contents. */ +async function readStdin(): Promise { + return new Promise((resolve, reject) => { + let fileData = ""; + + process.stdin.setEncoding("utf8"); + + process.stdin.on("readable", () => { + let chunk: string | Buffer | null = null; + + // tslint:disable-next-line: no-conditional-assignment + while ((chunk = process.stdin.read()) !== null) { + fileData += chunk; + } + }); + + process.stdin.on("end", () => { + resolve(fileData); + }); + + process.stdin.on("error", (error: Error) => { + reject(error); + }); + }); +} + +interface ResolvedFile { + filename: string; + isStdin: boolean; +} + +function resolveProgram( + { project }: Options, +): ts.Program | undefined { + if (project !== undefined) { + const projectPath = findTsconfig(project); + + if (projectPath === undefined) { + throw new FatalError(`Invalid option for project: ${project}`); + } + + return Linter.createProgram(projectPath); + } + + return undefined; +} + +function resolveFiles( + { files, stdin, stdinFilename, exclude, outputAbsolutePaths }: Options, + program: ts.Program | undefined, logger: Logger, -): { files: string[]; program?: ts.Program } { +): ResolvedFile[] { + // Return stdin files if we're reading from stdin. + if (stdin) { + if (stdinFilename) { + // Use a filename for shadowing stdin, if the option is set. + return [{filename: path.resolve(stdinFilename), isStdin: true}]; + } else { + return [{filename: "", isStdin: true}]; + } + } + // remove single quotes which break matching on Windows when glob is passed in single quotes exclude = exclude.map(trimSingleQuotes); - if (project === undefined) { - return { files: resolveGlobs(files, exclude, outputAbsolutePaths, logger) }; - } - - const projectPath = findTsconfig(project); - if (projectPath === undefined) { - throw new FatalError(`Invalid option for project: ${project}`); + if (program === undefined) { + return resolveGlobs(files, exclude, outputAbsolutePaths, logger) + .map((filename) => ({filename, isStdin: false})); } exclude = exclude.map((pattern) => path.resolve(pattern)); - const program = Linter.createProgram(projectPath); + let filesFound: string[]; + if (files.length === 0) { filesFound = filterFiles(Linter.getFileNames(program), exclude, false); } else { @@ -209,7 +276,8 @@ function resolveFilesAndProgram( } } } - return { files: filesFound, program }; + + return filesFound.map((filename) => ({filename, isStdin: false})); } function filterFiles(files: string[], patterns: string[], include: boolean): string[] { @@ -235,7 +303,7 @@ function resolveGlobs(files: string[], ignore: string[], outputAbsolutePaths: bo return results.map((file) => outputAbsolutePaths ? path.resolve(cwd, file) : path.relative(cwd, file)); } -async function doLinting(options: Options, files: string[], program: ts.Program | undefined, logger: Logger): Promise { +async function doLinting(options: Options, files: ResolvedFile[], program: ts.Program | undefined, logger: Logger): Promise { const linter = new Linter( { fix: !!options.fix, @@ -248,7 +316,9 @@ async function doLinting(options: Options, files: string[], program: ts.Program let lastFolder: string | undefined; let configFile = options.config !== undefined ? findConfiguration(options.config).results : undefined; - for (const file of files) { + for (const resolvedFile of files) { + const file = resolvedFile.filename; + if (options.config === undefined) { const folder = path.dirname(file); if (lastFolder !== folder) { @@ -261,7 +331,10 @@ async function doLinting(options: Options, files: string[], program: ts.Program } let contents: string | undefined; - if (program !== undefined) { + + if (resolvedFile.isStdin) { + contents = await readStdin(); + } else if (program !== undefined) { const sourceFile = program.getSourceFile(file); if (sourceFile !== undefined) { contents = sourceFile.text; diff --git a/src/tslintCli.ts b/src/tslintCli.ts index 9871e6f8588..1387c782286 100644 --- a/src/tslintCli.ts +++ b/src/tslintCli.ts @@ -35,6 +35,8 @@ interface Argv { outputAbsolutePaths: boolean; project?: string; rulesDir?: string; + stdin?: boolean; + stdinFilename?: string; formattersDir: string; format?: string; typeCheck?: boolean; @@ -45,7 +47,11 @@ interface Argv { interface Option { short?: string; // Commander will camelCase option names. - name: keyof Argv | "rules-dir" | "formatters-dir" | "type-check"; + name: keyof Argv + | "rules-dir" + | "stdin-filename" + | "formatters-dir" + | "type-check"; type: "string" | "boolean" | "array"; describe: string; // Short, used for usage message description: string; // Long, used for `--help` @@ -130,6 +136,23 @@ const options: Option[] = [ rules directory, so rules in the user-provided rules directory with the same name as the base rules will not be loaded.`, }, + { + name: "stdin", + type: "string", + describe: "stdin input", + description: dedent` + Read and check a file via stdin.`, + }, + { + name: "stdin-filename", + type: "string", + describe: "stdin filename", + description: dedent` + When reading a file with --stdin, tell TSLint where the file + being checked resides on the filesystem. This option allows TSLint + to be used for checking for problems on the fly, and also support + loading project configuration files, etc.`, + }, { short: "s", name: "formatters-dir", @@ -232,11 +255,22 @@ if (parsed.unknown.length !== 0) { } const argv = commander.opts() as any as Argv; -if (!(argv.init || argv.test !== undefined || argv.project !== undefined || commander.args.length > 0)) { +if (!( + argv.init + || argv.test !== undefined + || argv.project !== undefined + || commander.args.length > 0 + || argv.stdin +)) { console.error("No files specified. Use --project to lint a project folder."); process.exit(1); } +if (argv.stdin && argv.stdinFilename === undefined) { + console.error("--stdin-filename must be used with --stdin."); + process.exit(1); +} + if (argv.typeCheck) { console.warn("--type-check is deprecated. You only need --project to enable rules which need type information."); if (argv.project === undefined) { @@ -263,6 +297,8 @@ run( outputAbsolutePaths: argv.outputAbsolutePaths, project: argv.project, rulesDirectory: argv.rulesDir, + stdin: argv.stdin, + stdinFilename: argv.stdinFilename, test: argv.test, typeCheck: argv.typeCheck, }, From c033b17043c9f2073de0ce9b64ec60d6b7410029 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 7 Jul 2018 12:03:59 +0100 Subject: [PATCH 2/2] Require --stdin-filename for --stdin for now --- src/runner.ts | 2 +- src/tslintCli.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/runner.ts b/src/runner.ts index cbfc073e234..346460395ed 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -243,7 +243,7 @@ function resolveFiles( // Use a filename for shadowing stdin, if the option is set. return [{filename: path.resolve(stdinFilename), isStdin: true}]; } else { - return [{filename: "", isStdin: true}]; + throw new Error("--stdin-filename must be provided with --stdin."); } } diff --git a/src/tslintCli.ts b/src/tslintCli.ts index 1387c782286..b572d8049be 100644 --- a/src/tslintCli.ts +++ b/src/tslintCli.ts @@ -141,7 +141,8 @@ const options: Option[] = [ type: "string", describe: "stdin input", description: dedent` - Read and check a file via stdin.`, + Read and check a file via stdin. The --stdin-filename option must + also be given.`, }, { name: "stdin-filename", @@ -256,13 +257,16 @@ if (parsed.unknown.length !== 0) { const argv = commander.opts() as any as Argv; if (!( - argv.init - || argv.test !== undefined - || argv.project !== undefined - || commander.args.length > 0 - || argv.stdin + argv.init + || argv.test !== undefined + || argv.project !== undefined + || commander.args.length > 0 + || argv.stdin )) { - console.error("No files specified. Use --project to lint a project folder."); + console.error( + "No files specified. Use --project to lint a project folder " + + "or use --stdin and --stdin-filename.", + ); process.exit(1); }