@mark1russell7/ste-lint 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +251 -0
- package/dist/config.js.map +1 -0
- package/dist/dictionary.d.ts +74 -0
- package/dist/dictionary.d.ts.map +1 -0
- package/dist/dictionary.js +169 -0
- package/dist/dictionary.js.map +1 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +181 -0
- package/dist/files.js.map +1 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +61 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/lexicon.d.ts +33 -0
- package/dist/lexicon.d.ts.map +1 -0
- package/dist/lexicon.js +71 -0
- package/dist/lexicon.js.map +1 -0
- package/dist/lint.d.ts +28 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +132 -0
- package/dist/lint.js.map +1 -0
- package/dist/rules/about-quantity.d.ts +3 -0
- package/dist/rules/about-quantity.d.ts.map +1 -0
- package/dist/rules/about-quantity.js +34 -0
- package/dist/rules/about-quantity.js.map +1 -0
- package/dist/rules/index.d.ts +8 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +35 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/ing-verb.d.ts +9 -0
- package/dist/rules/ing-verb.d.ts.map +1 -0
- package/dist/rules/ing-verb.js +60 -0
- package/dist/rules/ing-verb.js.map +1 -0
- package/dist/rules/latin-abbreviation.d.ts +3 -0
- package/dist/rules/latin-abbreviation.d.ts.map +1 -0
- package/dist/rules/latin-abbreviation.js +48 -0
- package/dist/rules/latin-abbreviation.js.map +1 -0
- package/dist/rules/missing-subject.d.ts +16 -0
- package/dist/rules/missing-subject.d.ts.map +1 -0
- package/dist/rules/missing-subject.js +138 -0
- package/dist/rules/missing-subject.js.map +1 -0
- package/dist/rules/modal-verb.d.ts +3 -0
- package/dist/rules/modal-verb.d.ts.map +1 -0
- package/dist/rules/modal-verb.js +72 -0
- package/dist/rules/modal-verb.js.map +1 -0
- package/dist/rules/noun-cluster.d.ts +17 -0
- package/dist/rules/noun-cluster.d.ts.map +1 -0
- package/dist/rules/noun-cluster.js +83 -0
- package/dist/rules/noun-cluster.js.map +1 -0
- package/dist/rules/paragraph-length.d.ts +3 -0
- package/dist/rules/paragraph-length.d.ts.map +1 -0
- package/dist/rules/paragraph-length.js +19 -0
- package/dist/rules/paragraph-length.js.map +1 -0
- package/dist/rules/passive-instruction.d.ts +15 -0
- package/dist/rules/passive-instruction.d.ts.map +1 -0
- package/dist/rules/passive-instruction.js +38 -0
- package/dist/rules/passive-instruction.js.map +1 -0
- package/dist/rules/pronouns.d.ts +4 -0
- package/dist/rules/pronouns.d.ts.map +1 -0
- package/dist/rules/pronouns.js +57 -0
- package/dist/rules/pronouns.js.map +1 -0
- package/dist/rules/rule.d.ts +46 -0
- package/dist/rules/rule.d.ts.map +1 -0
- package/dist/rules/rule.js +31 -0
- package/dist/rules/rule.js.map +1 -0
- package/dist/rules/semicolon.d.ts +3 -0
- package/dist/rules/semicolon.d.ts.map +1 -0
- package/dist/rules/semicolon.js +20 -0
- package/dist/rules/semicolon.js.map +1 -0
- package/dist/rules/sentence-length.d.ts +3 -0
- package/dist/rules/sentence-length.d.ts.map +1 -0
- package/dist/rules/sentence-length.js +31 -0
- package/dist/rules/sentence-length.js.map +1 -0
- package/dist/rules/unknown-word.d.ts +9 -0
- package/dist/rules/unknown-word.d.ts.map +1 -0
- package/dist/rules/unknown-word.js +49 -0
- package/dist/rules/unknown-word.js.map +1 -0
- package/dist/rules/word-list-matcher.d.ts +25 -0
- package/dist/rules/word-list-matcher.d.ts.map +1 -0
- package/dist/rules/word-list-matcher.js +60 -0
- package/dist/rules/word-list-matcher.js.map +1 -0
- package/dist/rules/word-list.d.ts +3 -0
- package/dist/rules/word-list.d.ts.map +1 -0
- package/dist/rules/word-list.js +26 -0
- package/dist/rules/word-list.js.map +1 -0
- package/dist/run-cli.d.ts +27 -0
- package/dist/run-cli.d.ts.map +1 -0
- package/dist/run-cli.js +176 -0
- package/dist/run-cli.js.map +1 -0
- package/dist/source/blocks.d.ts +55 -0
- package/dist/source/blocks.d.ts.map +1 -0
- package/dist/source/blocks.js +464 -0
- package/dist/source/blocks.js.map +1 -0
- package/dist/source/inline.d.ts +42 -0
- package/dist/source/inline.d.ts.map +1 -0
- package/dist/source/inline.js +396 -0
- package/dist/source/inline.js.map +1 -0
- package/dist/source/jsx.d.ts +18 -0
- package/dist/source/jsx.d.ts.map +1 -0
- package/dist/source/jsx.js +137 -0
- package/dist/source/jsx.js.map +1 -0
- package/dist/source/lines.d.ts +34 -0
- package/dist/source/lines.d.ts.map +1 -0
- package/dist/source/lines.js +83 -0
- package/dist/source/lines.js.map +1 -0
- package/dist/source/markdown.d.ts +23 -0
- package/dist/source/markdown.d.ts.map +1 -0
- package/dist/source/markdown.js +42 -0
- package/dist/source/markdown.js.map +1 -0
- package/dist/source/suppressions.d.ts +38 -0
- package/dist/source/suppressions.d.ts.map +1 -0
- package/dist/source/suppressions.js +69 -0
- package/dist/source/suppressions.js.map +1 -0
- package/dist/source/tsdoc.d.ts +40 -0
- package/dist/source/tsdoc.d.ts.map +1 -0
- package/dist/source/tsdoc.js +176 -0
- package/dist/source/tsdoc.js.map +1 -0
- package/dist/text/grammar.d.ts +91 -0
- package/dist/text/grammar.d.ts.map +1 -0
- package/dist/text/grammar.js +289 -0
- package/dist/text/grammar.js.map +1 -0
- package/dist/text/phrases.d.ts +30 -0
- package/dist/text/phrases.d.ts.map +1 -0
- package/dist/text/phrases.js +65 -0
- package/dist/text/phrases.js.map +1 -0
- package/dist/text/sentences.d.ts +45 -0
- package/dist/text/sentences.d.ts.map +1 -0
- package/dist/text/sentences.js +141 -0
- package/dist/text/sentences.js.map +1 -0
- package/dist/text/tokens.d.ts +35 -0
- package/dist/text/tokens.d.ts.map +1 -0
- package/dist/text/tokens.js +113 -0
- package/dist/text/tokens.js.map +1 -0
- package/dist/text/words.d.ts +58 -0
- package/dist/text/words.d.ts.map +1 -0
- package/dist/text/words.js +159 -0
- package/dist/text/words.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/dist/units.d.ts +47 -0
- package/dist/units.d.ts.map +1 -0
- package/dist/units.js +42 -0
- package/dist/units.js.map +1 -0
- package/dist/word-list.d.ts +34 -0
- package/dist/word-list.d.ts.map +1 -0
- package/dist/word-list.js +90 -0
- package/dist/word-list.js.map +1 -0
- package/package.json +46 -3
- package/src/cli.ts +9 -0
- package/src/config.test.ts +188 -0
- package/src/config.ts +318 -0
- package/src/dictionary.test.ts +82 -0
- package/src/dictionary.ts +177 -0
- package/src/files.test.ts +107 -0
- package/src/files.ts +191 -0
- package/src/format.ts +78 -0
- package/src/index.test.ts +28 -0
- package/src/index.ts +34 -0
- package/src/lexicon.ts +92 -0
- package/src/lint.test.ts +182 -0
- package/src/lint.ts +135 -0
- package/src/rules/about-quantity.ts +33 -0
- package/src/rules/index.ts +40 -0
- package/src/rules/ing-verb.test.ts +48 -0
- package/src/rules/ing-verb.ts +54 -0
- package/src/rules/latin-abbreviation.ts +46 -0
- package/src/rules/missing-subject.test.ts +53 -0
- package/src/rules/missing-subject.ts +129 -0
- package/src/rules/modal-verb.test.ts +45 -0
- package/src/rules/modal-verb.ts +72 -0
- package/src/rules/noun-cluster.test.ts +49 -0
- package/src/rules/noun-cluster.ts +82 -0
- package/src/rules/paragraph-length.test.ts +30 -0
- package/src/rules/paragraph-length.ts +19 -0
- package/src/rules/passive-instruction.test.ts +38 -0
- package/src/rules/passive-instruction.ts +37 -0
- package/src/rules/pronouns.test.ts +38 -0
- package/src/rules/pronouns.ts +54 -0
- package/src/rules/punctuation.test.ts +59 -0
- package/src/rules/rule.ts +73 -0
- package/src/rules/semicolon.ts +19 -0
- package/src/rules/sentence-length.test.ts +52 -0
- package/src/rules/sentence-length.ts +32 -0
- package/src/rules/unknown-word.test.ts +81 -0
- package/src/rules/unknown-word.ts +43 -0
- package/src/rules/word-list-matcher.ts +68 -0
- package/src/rules/word-list.test.ts +110 -0
- package/src/rules/word-list.ts +25 -0
- package/src/run-cli.test.ts +189 -0
- package/src/run-cli.ts +193 -0
- package/src/source/blocks.ts +534 -0
- package/src/source/inline.test.ts +92 -0
- package/src/source/inline.ts +416 -0
- package/src/source/jsx.test.ts +46 -0
- package/src/source/jsx.ts +147 -0
- package/src/source/lines.test.ts +51 -0
- package/src/source/lines.ts +91 -0
- package/src/source/markdown.test.ts +210 -0
- package/src/source/markdown.ts +49 -0
- package/src/source/suppressions.ts +79 -0
- package/src/source/tsdoc.test.ts +137 -0
- package/src/source/tsdoc.ts +189 -0
- package/src/text/grammar.ts +304 -0
- package/src/text/phrases.ts +72 -0
- package/src/text/sentences.ts +148 -0
- package/src/text/text.test.ts +262 -0
- package/src/text/tokens.ts +135 -0
- package/src/text/words.ts +183 -0
- package/src/types.ts +39 -0
- package/src/units.ts +86 -0
- package/src/word-list.ts +128 -0
- package/ste.config.schema.json +87 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Linter } from "./lint.js";
|
|
6
|
+
import { parseArgs, runCli, UsageError, USAGE } from "./run-cli.js";
|
|
7
|
+
|
|
8
|
+
let root : string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
root = mkdtempSync(join(tmpdir(), "ste-cli-"));
|
|
12
|
+
writeFileSync(join(root, "ste.config.json"), JSON.stringify({ include : ["README.md", "docs/**/*.md", "src/**/*.ts"] }));
|
|
13
|
+
mkdirSync(join(root, "docs"));
|
|
14
|
+
mkdirSync(join(root, "src"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(root, { recursive : true, force : true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function run(argv : string[], env : Record<string, string | undefined> = {}) : { code : number; stdout : string; stderr : string } {
|
|
22
|
+
let stdout = "";
|
|
23
|
+
let stderr = "";
|
|
24
|
+
const code = runCli(argv, {
|
|
25
|
+
cwd : root,
|
|
26
|
+
env,
|
|
27
|
+
stdout : (text) => { stdout += text; },
|
|
28
|
+
stderr : (text) => { stderr += text; },
|
|
29
|
+
});
|
|
30
|
+
return { code, stdout, stderr };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("parseArgs", () => {
|
|
34
|
+
it("reads the options and the patterns", () => {
|
|
35
|
+
expect(parseArgs(["--format", "json", "--max-warnings=3", "--config", "c.json", "a.md", "docs/**"])).toEqual({
|
|
36
|
+
patterns : ["a.md", "docs/**"],
|
|
37
|
+
format : "json",
|
|
38
|
+
maxWarnings : 3,
|
|
39
|
+
configPath : "c.json",
|
|
40
|
+
help : false,
|
|
41
|
+
});
|
|
42
|
+
expect(parseArgs(["-h"]).help).toBe(true);
|
|
43
|
+
expect(parseArgs(["--format=text", "-"]).patterns).toEqual(["-"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects incorrect options", () => {
|
|
47
|
+
expect(() => parseArgs(["--format", "xml"])).toThrow(UsageError);
|
|
48
|
+
expect(() => parseArgs(["--max-warnings", "-1"])).toThrow("integer");
|
|
49
|
+
expect(() => parseArgs(["--max-warnings", "many"])).toThrow("integer");
|
|
50
|
+
expect(() => parseArgs(["--config"])).toThrow("needs a value");
|
|
51
|
+
expect(() => parseArgs(["--nope"])).toThrow("not a known option");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("runCli", () => {
|
|
56
|
+
it("shows the help", () => {
|
|
57
|
+
const result = run(["--help"]);
|
|
58
|
+
expect(result.code).toBe(0);
|
|
59
|
+
expect(result.stdout).toBe(USAGE);
|
|
60
|
+
expect(USAGE).toContain("automated approximation");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("exits with 0 when there are no problems", () => {
|
|
64
|
+
writeFileSync(join(root, "README.md"), "The monitor measures lag.\n");
|
|
65
|
+
const result = run([]);
|
|
66
|
+
expect(result.code).toBe(0);
|
|
67
|
+
expect(result.stdout).toContain("No problems found in 1 file.");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("exits with 1 when there are errors, and lints the default patterns of the configuration", () => {
|
|
71
|
+
writeFileSync(join(root, "README.md"), "You should stop.\n");
|
|
72
|
+
writeFileSync(join(root, "docs", "guide.md"), "Text; more text.\n");
|
|
73
|
+
writeFileSync(join(root, "src", "a.ts"), "/** Returns the value. */\nexport const a = 1;\n");
|
|
74
|
+
writeFileSync(join(root, "notes.txt"), "You should stop.\n");
|
|
75
|
+
const result = run([]);
|
|
76
|
+
expect(result.code).toBe(1);
|
|
77
|
+
expect(result.stdout).toContain("README.md\n 1:5");
|
|
78
|
+
expect(result.stdout).toContain("docs/guide.md");
|
|
79
|
+
expect(result.stdout).toContain("src/a.ts");
|
|
80
|
+
expect(result.stdout).not.toContain("notes.txt");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("writes JSON", () => {
|
|
84
|
+
writeFileSync(join(root, "README.md"), "Our tool.\n");
|
|
85
|
+
const result = run(["--format", "json"]);
|
|
86
|
+
expect(result.code).toBe(0);
|
|
87
|
+
const json = JSON.parse(result.stdout) as { summary : { warnings : number }; findings : Array<{ file : string; ruleId : string }> };
|
|
88
|
+
expect(json.summary.warnings).toBe(1);
|
|
89
|
+
expect(json.findings[0]).toMatchObject({ file : "README.md", ruleId : "first-person" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("exits with 1 when there are more warnings than --max-warnings", () => {
|
|
93
|
+
writeFileSync(join(root, "README.md"), "Our tool. Our page.\n");
|
|
94
|
+
expect(run(["--max-warnings", "2"]).code).toBe(0);
|
|
95
|
+
const result = run(["--max-warnings", "1"]);
|
|
96
|
+
expect(result.code).toBe(1);
|
|
97
|
+
expect(result.stderr).toContain("2 warnings. The maximum is 1.");
|
|
98
|
+
expect(run(["--max-warnings", "1", "--format", "json"]).stderr).toBe("");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("lints the patterns of the command line, relative to the working directory", () => {
|
|
102
|
+
writeFileSync(join(root, "other.md"), "You should stop.\n");
|
|
103
|
+
writeFileSync(join(root, "README.md"), "You should stop.\n");
|
|
104
|
+
const result = run(["other.md"]);
|
|
105
|
+
expect(result.stdout).toContain("other.md");
|
|
106
|
+
expect(result.stdout).not.toContain("README.md");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("applies the ignore patterns of the configuration relative to its directory, also from a subdirectory", () => {
|
|
110
|
+
writeFileSync(join(root, "ste.config.json"), JSON.stringify({ include : ["docs/**/*.md"], ignore : ["docs/legacy/**"] }));
|
|
111
|
+
mkdirSync(join(root, "docs", "legacy"));
|
|
112
|
+
writeFileSync(join(root, "docs", "legacy", "old.md"), "You should stop.\n");
|
|
113
|
+
writeFileSync(join(root, "docs", "guide.md"), "You should stop.\n");
|
|
114
|
+
let stdout = "";
|
|
115
|
+
const code = runCli(["**/*.md"], { cwd : join(root, "docs"), env : {}, stdout : (text) => { stdout += text; }, stderr : () => {} });
|
|
116
|
+
expect(code).toBe(1);
|
|
117
|
+
expect(stdout).toContain("guide.md");
|
|
118
|
+
expect(stdout).not.toContain("old.md");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("exits with 2 when it cannot read a directory", () => {
|
|
122
|
+
writeFileSync(join(root, "README.md"), "Text.\n");
|
|
123
|
+
// The walk reads README.md as a directory, and readdirSync fails with ENOTDIR
|
|
124
|
+
const result = run(["README.md/**/*.md"]);
|
|
125
|
+
expect(result.code).toBe(2);
|
|
126
|
+
expect(result.stderr).toMatch(/^ste-lint: cannot read the files: .*ENOTDIR/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("exits with 2 when the patterns match no files, and with 0 when the default patterns match no files", () => {
|
|
130
|
+
expect(run(["missing/*.md"])).toMatchObject({ code : 2, stderr : "ste-lint: no files match the patterns.\n" });
|
|
131
|
+
expect(run([])).toMatchObject({ code : 0, stderr : "ste-lint: no files to examine.\n" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("exits with 2 for an incorrect command line or configuration", () => {
|
|
135
|
+
expect(run(["--format", "xml"]).code).toBe(2);
|
|
136
|
+
writeFileSync(join(root, "ste.config.json"), JSON.stringify({ rules : { nope : "error" } }));
|
|
137
|
+
const result = run([]);
|
|
138
|
+
expect(result.code).toBe(2);
|
|
139
|
+
expect(result.stderr).toContain("is not a known rule");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses a dictionary from STE_DICTIONARY", () => {
|
|
143
|
+
writeFileSync(join(root, "README.md"), "The valve opens the zorblat.\n");
|
|
144
|
+
writeFileSync(join(root, "dictionary.json"), JSON.stringify(["the", "valve", "opens"]));
|
|
145
|
+
const result = run(["--format", "json"], { STE_DICTIONARY : "dictionary.json" });
|
|
146
|
+
const json = JSON.parse(result.stdout) as { findings : Array<{ ruleId : string; message : string }> };
|
|
147
|
+
expect(json.findings.map((finding) => finding.ruleId)).toEqual(["unknown-word"]);
|
|
148
|
+
expect(json.findings[0]!.message).toContain("\"zorblat\"");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("tells the user when the dictionary file does not exist, and continues without it", () => {
|
|
152
|
+
writeFileSync(join(root, "README.md"), "The valve opens the zorblat.\n");
|
|
153
|
+
const result = run([], { STE_DICTIONARY : "missing.json" });
|
|
154
|
+
expect(result.code).toBe(0);
|
|
155
|
+
expect(result.stderr).toContain("does not exist. The unknown-word rule is off.");
|
|
156
|
+
writeFileSync(join(root, "ste.config.json"), JSON.stringify({ include : ["README.md"], dictionaryPath : "missing.json" }));
|
|
157
|
+
expect(run([]).stderr).toContain("(from dictionaryPath)");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("exits with 2 for a dictionary file that is not valid", () => {
|
|
161
|
+
writeFileSync(join(root, "README.md"), "Text.\n");
|
|
162
|
+
writeFileSync(join(root, "dictionary.json"), "{");
|
|
163
|
+
expect(run([], { STE_DICTIONARY : "dictionary.json" }).code).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("exits with 2 when it cannot examine a file, and examines the other files", () => {
|
|
167
|
+
writeFileSync(join(root, "README.md"), "You should stop.\n");
|
|
168
|
+
writeFileSync(join(root, "docs", "guide.md"), "Text.\n");
|
|
169
|
+
const original = Linter.prototype.lintFile;
|
|
170
|
+
const spy = vi.spyOn(Linter.prototype, "lintFile").mockImplementation(function (this : Linter, path : string, shown? : string) {
|
|
171
|
+
if (path.endsWith("guide.md")) throw new Error("cannot read");
|
|
172
|
+
return original.call(this, path, shown);
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
const result = run([]);
|
|
176
|
+
expect(result.code).toBe(2);
|
|
177
|
+
expect(result.stderr).toContain("cannot examine");
|
|
178
|
+
expect(result.stderr).toContain("cannot read");
|
|
179
|
+
expect(result.stdout).toContain("README.md");
|
|
180
|
+
} finally {
|
|
181
|
+
spy.mockRestore();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("does not catch errors that are not configuration errors", () => {
|
|
186
|
+
const env = new Proxy({}, { get : () => { throw new TypeError("unexpected"); } });
|
|
187
|
+
expect(() => runCli([], { cwd : root, env, stdout : () => {}, stderr : () => {} })).toThrow("unexpected");
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/run-cli.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command-line interface. `runCli` does not use `process` directly, so
|
|
3
|
+
* the tests can give it arguments, an environment and output functions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { relative, resolve, sep } from "node:path";
|
|
8
|
+
import { ConfigError, DICTIONARY_ENV, loadConfig, type ResolvedConfig } from "./config.js";
|
|
9
|
+
import { DictionaryError, loadDictionary, type Dictionary } from "./dictionary.js";
|
|
10
|
+
import { findFiles } from "./files.js";
|
|
11
|
+
import { formatJson, formatText, summarize } from "./format.js";
|
|
12
|
+
import { buildLexicon } from "./lexicon.js";
|
|
13
|
+
import { fileKind, Linter } from "./lint.js";
|
|
14
|
+
import type { Finding } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export type CliIo = {
|
|
17
|
+
readonly cwd : string;
|
|
18
|
+
readonly env : Readonly<Record<string, string | undefined>>;
|
|
19
|
+
readonly stdout : (text : string) => void;
|
|
20
|
+
readonly stderr : (text : string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type CliOptions = {
|
|
24
|
+
readonly patterns : readonly string[];
|
|
25
|
+
readonly format : "text" | "json";
|
|
26
|
+
readonly maxWarnings : number | null;
|
|
27
|
+
readonly configPath : string | null;
|
|
28
|
+
readonly help : boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** An incorrect command line. */
|
|
32
|
+
export class UsageError extends Error {
|
|
33
|
+
override readonly name : string = "UsageError";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const USAGE : string = `Usage: ste-lint [options] [patterns...]
|
|
37
|
+
|
|
38
|
+
ste-lint examines the prose in Markdown files, MDX files and TypeScript doc
|
|
39
|
+
comments. It finds text that does not obey the writing rules of ASD-STE100
|
|
40
|
+
Simplified Technical English. The tool is an automated approximation of
|
|
41
|
+
these rules. It does not certify that a text obeys the standard.
|
|
42
|
+
|
|
43
|
+
Without patterns, ste-lint uses the "include" patterns of ste.config.json,
|
|
44
|
+
or the default patterns. Patterns are relative to the working directory.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--format <text|json> The output format. The default is "text".
|
|
48
|
+
--max-warnings <n> Exit with code 1 when there are more than n warnings.
|
|
49
|
+
--config <path> Use this configuration file.
|
|
50
|
+
-h, --help Show this help.
|
|
51
|
+
|
|
52
|
+
Exit codes:
|
|
53
|
+
0 No errors, and not more warnings than the maximum.
|
|
54
|
+
1 One or more errors, or more warnings than the maximum.
|
|
55
|
+
2 An incorrect command line or configuration, or no files.
|
|
56
|
+
|
|
57
|
+
Environment:
|
|
58
|
+
${DICTIONARY_ENV} The path of a local dictionary file. This path has
|
|
59
|
+
priority over "dictionaryPath" in ste.config.json.
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
function optionValue(argv : readonly string[], index : number, name : string) : string {
|
|
63
|
+
const value = argv[index + 1];
|
|
64
|
+
if (value === undefined || value.startsWith("--")) throw new UsageError(`The option ${name} needs a value.`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** This function reads the command-line arguments. */
|
|
69
|
+
export function parseArgs(argv : readonly string[]) : CliOptions {
|
|
70
|
+
const patterns : string[] = [];
|
|
71
|
+
let format : CliOptions["format"] = "text";
|
|
72
|
+
let maxWarnings : number | null = null;
|
|
73
|
+
let configPath : string | null = null;
|
|
74
|
+
let help = false;
|
|
75
|
+
for (let i = 0; i < argv.length; i++) {
|
|
76
|
+
const argument = argv[i]!;
|
|
77
|
+
const [name, inline] = argument.startsWith("--") && argument.includes("=") ? [argument.slice(0, argument.indexOf("=")), argument.slice(argument.indexOf("=") + 1)] : [argument, undefined];
|
|
78
|
+
const value = () : string => {
|
|
79
|
+
if (inline !== undefined) return inline;
|
|
80
|
+
const next = optionValue(argv, i, name);
|
|
81
|
+
i++;
|
|
82
|
+
return next;
|
|
83
|
+
};
|
|
84
|
+
switch (name) {
|
|
85
|
+
case "-h":
|
|
86
|
+
case "--help":
|
|
87
|
+
help = true;
|
|
88
|
+
break;
|
|
89
|
+
case "--format": {
|
|
90
|
+
const text = value();
|
|
91
|
+
if (text !== "text" && text !== "json") throw new UsageError(`The format must be "text" or "json", not "${text}".`);
|
|
92
|
+
format = text;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "--max-warnings": {
|
|
96
|
+
const text = value();
|
|
97
|
+
const count = Number(text);
|
|
98
|
+
if (!/^\d+$/.test(text) || !Number.isSafeInteger(count)) throw new UsageError(`--max-warnings must be an integer of 0 or more, not "${text}".`);
|
|
99
|
+
maxWarnings = count;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "--config":
|
|
103
|
+
configPath = value();
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
if (argument.startsWith("-") && argument !== "-") throw new UsageError(`"${argument}" is not a known option. Use --help to show the options.`);
|
|
107
|
+
patterns.push(argument);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { patterns, format, maxWarnings, configPath, help };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function displayPath(path : string, cwd : string) : string {
|
|
114
|
+
const shown = relative(cwd, path);
|
|
115
|
+
return (shown.length > 0 && !shown.startsWith("..") ? shown : path).split(sep).join("/");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadOptionalDictionary(config : ResolvedConfig, io : CliIo) : Dictionary | null {
|
|
119
|
+
if (config.dictionaryPath === null) return null;
|
|
120
|
+
if (!existsSync(config.dictionaryPath)) {
|
|
121
|
+
const from = config.dictionarySource === "env" ? DICTIONARY_ENV : "dictionaryPath";
|
|
122
|
+
io.stderr(`ste-lint: the dictionary file ${config.dictionaryPath} (from ${from}) does not exist. The unknown-word rule is off.\n`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return loadDictionary(config.dictionaryPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** This function starts the linter with command-line arguments. The result is the exit code. */
|
|
129
|
+
export function runCli(argv : readonly string[], io : CliIo) : number {
|
|
130
|
+
let options : CliOptions;
|
|
131
|
+
try {
|
|
132
|
+
options = parseArgs(argv);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
io.stderr(`ste-lint: ${(error as Error).message}\n`);
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
if (options.help) {
|
|
138
|
+
io.stdout(USAGE);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let config : ResolvedConfig;
|
|
143
|
+
let dictionary : Dictionary | null;
|
|
144
|
+
try {
|
|
145
|
+
config = loadConfig({ cwd : io.cwd, env : io.env, ...(options.configPath === null ? {} : { configPath : options.configPath }) });
|
|
146
|
+
dictionary = loadOptionalDictionary(config, io);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof ConfigError || error instanceof DictionaryError) {
|
|
149
|
+
io.stderr(`ste-lint: ${error.message}\n`);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const explicit = options.patterns.length > 0;
|
|
156
|
+
let files : string[];
|
|
157
|
+
try {
|
|
158
|
+
// The patterns of the command line are relative to the working directory. The ignore patterns are relative to the configuration.
|
|
159
|
+
files = (explicit
|
|
160
|
+
? findFiles({ root : io.cwd, include : options.patterns, ignore : config.ignore, ignoreRoot : config.root })
|
|
161
|
+
: findFiles({ root : config.root, include : config.include, ignore : config.ignore })
|
|
162
|
+
).filter((file) => fileKind(file) !== null);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// For example, a directory that the user cannot read (EACCES)
|
|
165
|
+
io.stderr(`ste-lint: cannot read the files: ${(error as Error).message}\n`);
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
io.stderr(explicit ? "ste-lint: no files match the patterns.\n" : "ste-lint: no files to examine.\n");
|
|
170
|
+
return explicit ? 2 : 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const linter = new Linter(config, buildLexicon(config, dictionary));
|
|
174
|
+
const findings : Finding[] = [];
|
|
175
|
+
let failed = false;
|
|
176
|
+
for (const file of files) {
|
|
177
|
+
try {
|
|
178
|
+
findings.push(...linter.lintFile(file, displayPath(file, resolve(io.cwd))));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
io.stderr(`ste-lint: cannot examine ${file}: ${(error as Error).message}\n`);
|
|
181
|
+
failed = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const summary = summarize(findings, files.length);
|
|
186
|
+
io.stdout(options.format === "json" ? formatJson(findings, summary) : formatText(findings, summary));
|
|
187
|
+
const tooManyWarnings = options.maxWarnings !== null && summary.warnings > options.maxWarnings;
|
|
188
|
+
if (tooManyWarnings && options.format === "text") {
|
|
189
|
+
io.stderr(`ste-lint: ${summary.warnings} warnings. The maximum is ${options.maxWarnings}.\n`);
|
|
190
|
+
}
|
|
191
|
+
if (failed) return 2;
|
|
192
|
+
return summary.errors > 0 || tooManyWarnings ? 1 : 0;
|
|
193
|
+
}
|