Skip to content

Commit 2317880

Browse files
committed
feat(cli): Display error in context with the source file's contents.
1 parent f7f2dfb commit 2317880

File tree

5 files changed

+133
-16
lines changed

5 files changed

+133
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { SourceRange } from "@css-blocks/core";
2+
import * as fs from "fs";
3+
export interface ExtractionResult {
4+
lines: string[];
5+
additionalLines: {
6+
before: number;
7+
after: number;
8+
};
9+
}
10+
export function extractLinesFromSource(
11+
range: Required<SourceRange>,
12+
additionalLinesBefore = 1,
13+
additionalLinesAfter = 0,
14+
): ExtractionResult {
15+
let { filename, start, end } = range;
16+
let contents = fs.readFileSync(filename, "utf-8");
17+
let allLines = contents.split(/\r?\n/);
18+
if (start.line <= additionalLinesBefore) {
19+
additionalLinesBefore = start.line - 1;
20+
}
21+
if (end.line + additionalLinesAfter > allLines.length ) {
22+
additionalLinesAfter = allLines.length - end.line;
23+
}
24+
let firstLine = start.line - additionalLinesBefore - 1;
25+
let lastLine = end.line + additionalLinesAfter;
26+
let lines = allLines.slice(firstLine, lastLine);
27+
return {
28+
lines,
29+
additionalLines: {
30+
before: additionalLinesBefore,
31+
after: additionalLinesAfter,
32+
},
33+
};
34+
}

packages/@css-blocks/cli/src/index.ts

+79-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { BlockFactory, CssBlockError, Importer, NodeJsImporter, Preprocessors, hasErrorPosition } from "@css-blocks/core";
1+
import { BlockFactory, CssBlockError, ErrorWithPosition, Importer, NodeJsImporter, Preprocessors, hasErrorPosition } from "@css-blocks/core";
22
import chalk = require("chalk");
33
import fse = require("fs-extra");
44
import path = require("path");
55
import yargs = require("yargs");
66

7+
import { ExtractionResult, extractLinesFromSource } from "./extract-lines-from-source";
8+
79
type Aliases = ConstructorParameters<typeof NodeJsImporter>[0];
810

911
/**
@@ -123,33 +125,100 @@ export class CLI {
123125
let factory = new BlockFactory({preprocessors, importer});
124126
let errorCount = 0;
125127
for (let blockFile of blockFiles) {
128+
let blockFileRelative = path.relative(process.cwd(), path.resolve(blockFile));
126129
try {
127130
if (importer) {
128131
let ident = importer.identifier(null, blockFile, factory.configuration);
129-
blockFile = importer.filesystemPath(ident, factory.configuration) || blockFile;
132+
blockFile = importer.filesystemPath(ident, factory.configuration) || path.join(blockFile);
130133
}
131-
await factory.getBlockFromPath(blockFile);
134+
await factory.getBlockFromPath(path.resolve(blockFile));
132135
// if the above line doesn't throw then there wasn't a syntax error.
133-
this.println(`${this.chalk.green("ok")}\t${path.relative(process.cwd(), path.resolve(blockFile))}`);
136+
this.println(`${this.chalk.green("ok")}\t${this.chalk.whiteBright(blockFileRelative)}`);
134137
} catch (e) {
135138
errorCount++;
136139
if (e instanceof CssBlockError) {
137140
let loc = e.location;
138-
let filename = path.relative(process.cwd(), path.resolve(loc && loc.filename || blockFile));
139-
let message = `${this.chalk.red("error")}\t${this.chalk.whiteBright(filename)}`;
140-
if (hasErrorPosition(loc)) {
141-
message += `:${loc.start.line}:${loc.start.column}`;
141+
let message = `${this.chalk.red("error")}\t${this.chalk.whiteBright(blockFileRelative)}`;
142+
if (!hasErrorPosition(loc)) {
143+
this.println(message, e.origMessage);
144+
continue;
145+
} else {
146+
this.println(message);
147+
this.displayError(blockFileRelative, e);
142148
}
143-
message += ` ${e.origMessage}`;
144-
this.println(message);
145149
} else {
146150
console.error(e);
147151
}
148152
}
149153
}
154+
if (errorCount) {
155+
this.println(`Found ${this.chalk.redBright(`${errorCount} error${errorCount > 1 ? "s" : ""}`)} in ${blockFiles.length} file${blockFiles.length > 1 ? "s" : ""}.`);
156+
}
150157
this.exit(errorCount);
151158
}
152159

160+
displayError(blockFileRelative: string, e: CssBlockError) {
161+
let loc = e.location;
162+
if (!hasErrorPosition(loc)) {
163+
return;
164+
}
165+
loc.end.line = 4;
166+
let filename = path.relative(process.cwd(), path.resolve(loc && loc.filename || blockFileRelative));
167+
let context: ExtractionResult | undefined;
168+
let lineNumber: number | undefined;
169+
context = extractLinesFromSource(loc, 1, 1);
170+
lineNumber = loc.start.line - context.additionalLines.before;
171+
if (context) {
172+
this.println(
173+
this.chalk.bold.white("\tAt"),
174+
this.chalk.bold.whiteBright(`${filename}:${loc.start.line}:${loc.start.column}`),
175+
`${e.origMessage}`,
176+
);
177+
for (let i = 0; i < context.lines.length; i++) {
178+
let prefix;
179+
let line = context.lines[i];
180+
if (i < context.additionalLines.before ||
181+
i >= context.lines.length - context.additionalLines.after) {
182+
prefix = this.chalk.bold(`${lineNumber}: `);
183+
} else {
184+
prefix = this.chalk.bold.redBright(`${lineNumber}: `);
185+
let {before, during, after } = this.splitLineOnErrorRange(line, lineNumber, loc);
186+
line = `${before}${this.chalk.underline.redBright(during)}${after}`;
187+
}
188+
this.println("\t" + prefix + line);
189+
if (lineNumber) lineNumber++;
190+
}
191+
}
192+
}
193+
194+
splitLineOnErrorRange(line: string, lineNumber: number, loc: ErrorWithPosition) {
195+
if (lineNumber === loc.start.line && lineNumber === loc.end.line) {
196+
let before = line.slice(0, loc.start.column - 1);
197+
let during = line.slice(loc.start.column - 1, loc.end.column);
198+
let after = line.slice(loc.end.column);
199+
return { before, during, after };
200+
} else if (lineNumber === loc.start.line) {
201+
let before = line.slice(0, loc.start.column - 1);
202+
let during = line.slice(loc.start.column - 1);
203+
return { before, during, after: "" };
204+
} else if (lineNumber === loc.end.line) {
205+
let leadingWhitespace = "";
206+
let during = line.slice(0, loc.end.column);
207+
if (during.match(/^(\s+)/)) {
208+
leadingWhitespace = RegExp.$1;
209+
during = during.replace(/^\s+/, "");
210+
}
211+
let after = line.slice(loc.end.column);
212+
return {before: leadingWhitespace, during, after };
213+
} else {
214+
let leadingWhitespace = "";
215+
if (line.match(/^(\s+)/)) {
216+
leadingWhitespace = RegExp.$1;
217+
line = line.replace(/^\s+/, "");
218+
}
219+
return {before: leadingWhitespace, during: line, after: "" };
220+
}
221+
}
153222
/**
154223
* Instance method so tests can easily capture output.
155224
*/

packages/@css-blocks/cli/test/TestCLI.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export class TestCLI extends CLI {
88
this.output = "";
99
this.chalk.enabled = false;
1010
}
11-
println(text: string) {
12-
this.output += text + "\n";
11+
println(...texts: string[]) {
12+
this.output += texts.join(" ") + "\n";
1313
}
1414
argumentParser() {
1515
let parser = super.argumentParser();

packages/@css-blocks/cli/test/cli-test.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,27 @@ describe("validate", () => {
2929
it("can check syntax for a bad block file", async () => {
3030
let cli = new CLI();
3131
await cli.run(["validate", fixture("basic/error.block.css")]);
32-
assert.equal(cli.output, `error\t${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar\n`);
32+
assert.equal(cli.output,
33+
`error\t${relFixture("basic/error.block.css")}
34+
\tAt ${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar
35+
\t1: .foo.bar {
36+
\t2: color: red;
37+
\t3: }
38+
Found 1 error in 1 file.
39+
`);
3340
assert.equal(cli.exitCode, 1);
3441
});
3542
it("correctly displays errors in referenced blocks.", async () => {
3643
let cli = new CLI();
3744
await cli.run(["validate", fixture("basic/transitive-error.block.css")]);
38-
assert.equal(cli.output, `error\t${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar\n`);
45+
assert.equal(cli.output,
46+
`error\t${relFixture("basic/transitive-error.block.css")}
47+
\tAt ${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar
48+
\t1: .foo.bar {
49+
\t2: color: red;
50+
\t3: }
51+
Found 1 error in 1 file.
52+
`);
3953
assert.equal(cli.exitCode, 1);
4054
});
4155
it("can import from node_modules", async () => {
@@ -64,7 +78,7 @@ describe("validate with preprocessors", () => {
6478
it("can check syntax for a bad block file", async () => {
6579
let cli = new CLI();
6680
await cli.run(["validate", "--preprocessors", distFile("test/preprocessors"), fixture("scss/error.block.scss")]);
67-
assert.equal(cli.output, `error\t${relFixture("scss/error.block.scss")}:5:5 Two distinct classes cannot be selected on the same element: .foo.bar\n`);
81+
assert.equal(cli.output.split(/\n/)[1].trim(), `At ${relFixture("scss/error.block.scss")}:5:5 Two distinct classes cannot be selected on the same element: .foo.bar`);
6882
assert.equal(cli.exitCode, 1);
6983
});
7084
});

packages/@css-blocks/core/src/errors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class CssBlockError extends Error {
3939
if (!loc) {
4040
return this.origMessage;
4141
}
42-
let filename = loc.filename || "";
42+
let filename = loc.filename || "<unknown file>";
4343
let line: string | number = "";
4444
let column: string | number = "";
4545
if (hasErrorPosition(loc)) {

0 commit comments

Comments
 (0)