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

Implement --stdin and --stdin-filename CLI options #3816

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 89 additions & 16 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -157,7 +167,9 @@ async function runWorker(options: Options, logger: Logger): Promise<Status> {
}

async function runLinter(options: Options, logger: Logger): Promise<LintResult> {
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);
Expand All @@ -173,25 +185,80 @@ async function runLinter(options: Options, logger: Logger): Promise<LintResult>
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<string> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runner.ts is getting pretty huge. Please separate this standalone utility to a different file (`src/utils.ts, maybe?).

return new Promise<string>((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 {
throw new Error("--stdin-filename must be provided with --stdin.");
}
}

// 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 {
Expand All @@ -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[] {
Expand All @@ -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<LintResult> {
async function doLinting(options: Options, files: ResolvedFile[], program: ts.Program | undefined, logger: Logger): Promise<LintResult> {
const linter = new Linter(
{
fix: !!options.fix,
Expand All @@ -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) {
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is --std-filename being ignored when actually fetching content with --std?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially yes. The filename is a hint for where the file is, and the data comes from stdin.

} else if (program !== undefined) {
const sourceFile = program.getSourceFile(file);
if (sourceFile !== undefined) {
contents = sourceFile.text;
Expand Down
46 changes: 43 additions & 3 deletions src/tslintCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface Argv {
outputAbsolutePaths: boolean;
project?: string;
rulesDir?: string;
stdin?: boolean;
stdinFilename?: string;
formattersDir: string;
format?: string;
typeCheck?: boolean;
Expand All @@ -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`
Expand Down Expand Up @@ -130,6 +136,24 @@ 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. The --stdin-filename option must
also be given.`,
},
{
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",
Expand Down Expand Up @@ -232,8 +256,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)) {
console.error("No files specified. Use --project to lint a project folder.");
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 "
+ "or use --stdin and --stdin-filename.",
);
process.exit(1);
}

if (argv.stdin && argv.stdinFilename === undefined) {
console.error("--stdin-filename must be used with --stdin.");
process.exit(1);
}

Expand Down Expand Up @@ -263,6 +301,8 @@ run(
outputAbsolutePaths: argv.outputAbsolutePaths,
project: argv.project,
rulesDirectory: argv.rulesDir,
stdin: argv.stdin,
stdinFilename: argv.stdinFilename,
test: argv.test,
typeCheck: argv.typeCheck,
},
Expand Down