@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
package/src/lint.test.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { resolveConfig, type SteConfigFile } from "./config.js";
|
|
6
|
+
import { formatJson, formatText, summarize } from "./format.js";
|
|
7
|
+
import { fileKind, Linter, lintText } from "./lint.js";
|
|
8
|
+
import { getRule, RULES } from "./rules/index.js";
|
|
9
|
+
import { RULE_IDS, isRuleId, type Finding } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const config = (file : SteConfigFile = {}) => resolveConfig(file, { root : "/" });
|
|
12
|
+
const ids = (findings : readonly Finding[]) : string[] => findings.map((finding) => finding.ruleId);
|
|
13
|
+
|
|
14
|
+
describe("fileKind", () => {
|
|
15
|
+
it("knows Markdown, MDX and TypeScript files", () => {
|
|
16
|
+
expect(fileKind("a.md")).toBe("markdown");
|
|
17
|
+
expect(fileKind("a.markdown")).toBe("markdown");
|
|
18
|
+
expect(fileKind("a.MDX")).toBe("mdx");
|
|
19
|
+
expect(fileKind("a.ts")).toBe("typescript");
|
|
20
|
+
expect(fileKind("a.tsx")).toBe("typescript");
|
|
21
|
+
expect(fileKind("a.mjs")).toBe("typescript");
|
|
22
|
+
expect(fileKind("a.d.ts")).toBeNull();
|
|
23
|
+
expect(fileKind("a.json")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Linter on TSX files", () => {
|
|
28
|
+
it("examines the text that the user interface shows, and the doc comments", () => {
|
|
29
|
+
const source = [
|
|
30
|
+
"/** Returns the page. */",
|
|
31
|
+
"export function Page() {",
|
|
32
|
+
" return <p>The run may fail; <code>pnpm results</code> utilizes the data.</p>;",
|
|
33
|
+
"}",
|
|
34
|
+
].join("\n");
|
|
35
|
+
const findings = lintText(source, "Page.tsx", config());
|
|
36
|
+
expect(ids(findings)).toEqual(expect.arrayContaining(["missing-subject", "modal-verb", "semicolon", "word-list"]));
|
|
37
|
+
const modal = findings.find((finding) => finding.ruleId === "modal-verb")!;
|
|
38
|
+
expect([modal.line, modal.column]).toEqual([3, source.split("\n")[2]!.indexOf("may") + 1]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not examine the text of JSX in a .ts file", () => {
|
|
42
|
+
expect(lintText("const a = 'You should stop.';", "a.ts", config())).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("Linter", () => {
|
|
47
|
+
it("gives findings with the file, the position and the rule, in order", () => {
|
|
48
|
+
const findings = lintText("# Title\n\nYou should stop; it may fail.", "doc.md", config());
|
|
49
|
+
expect(findings.map((finding) => [finding.file, finding.line, finding.column, finding.ruleId, finding.severity])).toEqual([
|
|
50
|
+
["doc.md", 3, 5, "modal-verb", "error"],
|
|
51
|
+
["doc.md", 3, 16, "semicolon", "error"],
|
|
52
|
+
["doc.md", 3, 21, "modal-verb", "error"],
|
|
53
|
+
]);
|
|
54
|
+
expect(findings[0]).toMatchObject({ endLine : 3, endColumn : 11 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("gives correct positions in files with CRLF line ends", () => {
|
|
58
|
+
const markdown = lintText("# Title\r\n\r\nText. You should stop.\r\n", "doc.md", config());
|
|
59
|
+
expect(markdown.map((finding) => [finding.line, finding.column])).toEqual([[3, 11]]);
|
|
60
|
+
const source = lintText("/**\r\n * Text.\r\n * You should stop.\r\n */\r\nexport const a = 1;\r\n", "a.ts", config());
|
|
61
|
+
expect(source.map((finding) => [finding.line, finding.column])).toEqual([[3, 8]]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("gives correct positions after an HTML entity of a character outside the BMP", () => {
|
|
65
|
+
const text = "Text 😀 then you should stop.";
|
|
66
|
+
const findings = lintText(text, "doc.md", config());
|
|
67
|
+
expect(findings.map((finding) => [finding.ruleId, finding.column, finding.endColumn])).toEqual([["modal-verb", text.indexOf("should") + 1, text.indexOf("should") + 7]]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("gives no findings for a file kind that it cannot read", () => {
|
|
71
|
+
expect(lintText("You should stop.", "a.json", config())).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("applies severity overrides and turns rules off", () => {
|
|
75
|
+
const findings = lintText("You should stop; now.", "doc.md", config({ rules : { "modal-verb" : "warning", semicolon : "off" } }));
|
|
76
|
+
expect(findings.map((finding) => [finding.ruleId, finding.severity])).toEqual([
|
|
77
|
+
["modal-verb", "warning"],
|
|
78
|
+
["word-list", "error"],
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("runs only the word rules on headings and table cells", () => {
|
|
83
|
+
const long = Array.from({ length : 30 }, (_, i) => `word${i}`).join(" ");
|
|
84
|
+
const findings = lintText(`# ${long} should\n\n| ${long} may |\n| --- |\n| x |`, "doc.md", config());
|
|
85
|
+
expect(ids(findings)).toEqual(["modal-verb", "modal-verb"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("applies suppression comments in Markdown", () => {
|
|
89
|
+
const markdown = [
|
|
90
|
+
"<!-- ste-disable-next modal-verb -->",
|
|
91
|
+
"You should stop; it may fail.",
|
|
92
|
+
"",
|
|
93
|
+
"<!-- ste-disable -->",
|
|
94
|
+
"You should stop.",
|
|
95
|
+
"<!-- ste-enable -->",
|
|
96
|
+
"",
|
|
97
|
+
"You should stop.",
|
|
98
|
+
].join("\n");
|
|
99
|
+
expect(lintText(markdown, "doc.md", config()).map((finding) => [finding.line, finding.ruleId])).toEqual([
|
|
100
|
+
[2, "semicolon"],
|
|
101
|
+
[8, "modal-verb"],
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("applies suppression comments in TypeScript", () => {
|
|
106
|
+
const source = "// ste-disable-next\n/** You should stop; now. */\nexport const a = 1;\n/** You should stop. */\nexport const b = 2;\n";
|
|
107
|
+
expect(lintText(source, "a.ts", config()).map((finding) => [finding.line, finding.ruleId])).toEqual([[4, "modal-verb"]]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps an ste-enable for one rule from stopping a range for all rules", () => {
|
|
111
|
+
const markdown = "<!-- ste-disable semicolon modal-verb -->\nYou should; stop.\n<!-- ste-enable semicolon -->\nYou should; stop.\n\n<!-- ste-disable -->\n<!-- ste-enable semicolon -->\nYou should; stop.";
|
|
112
|
+
expect(lintText(markdown, "doc.md", config()).map((finding) => [finding.line, finding.ruleId])).toEqual([[4, "semicolon"]]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("gives the prose units of a file", () => {
|
|
116
|
+
const linter = new Linter(config());
|
|
117
|
+
const units = linter.units("# Title\n\nOne. Two.\n\n```\ncode\n```", "doc.md");
|
|
118
|
+
expect(units.map((unit) => [unit.kind, unit.sentences.length])).toEqual([["heading", 1], ["paragraph", 2]]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("reads a file from the disk", () => {
|
|
122
|
+
const directory = mkdtempSync(join(tmpdir(), "ste-lint-"));
|
|
123
|
+
try {
|
|
124
|
+
const path = join(directory, "doc.md");
|
|
125
|
+
writeFileSync(path, "You should stop.");
|
|
126
|
+
const linter = new Linter(config());
|
|
127
|
+
expect(linter.lintFile(path, "doc.md")[0]!.file).toBe("doc.md");
|
|
128
|
+
expect(linter.lintFile(path)[0]!.file).toBe(path);
|
|
129
|
+
} finally {
|
|
130
|
+
rmSync(directory, { recursive : true, force : true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("rules", () => {
|
|
136
|
+
it("has one rule for each rule ID, with a description and an STE reference", () => {
|
|
137
|
+
expect(RULES.map((rule) => rule.id)).toEqual([...RULE_IDS]);
|
|
138
|
+
for (const id of RULE_IDS) {
|
|
139
|
+
expect(getRule(id).description.length).toBeGreaterThan(10);
|
|
140
|
+
expect(getRule(id).ste.length).toBeGreaterThan(0);
|
|
141
|
+
}
|
|
142
|
+
expect(isRuleId("semicolon")).toBe(true);
|
|
143
|
+
expect(isRuleId("nope")).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("format", () => {
|
|
148
|
+
const findings = lintText("You should stop; now.\n\nOur tool.", "doc.md", config());
|
|
149
|
+
const summary = summarize(findings, 3);
|
|
150
|
+
|
|
151
|
+
it("counts the findings by severity and by rule", () => {
|
|
152
|
+
expect(summary).toEqual({
|
|
153
|
+
files : 3,
|
|
154
|
+
filesWithFindings : 1,
|
|
155
|
+
errors : 3,
|
|
156
|
+
warnings : 1,
|
|
157
|
+
byRule : {
|
|
158
|
+
"modal-verb" : { errors : 1, warnings : 0 },
|
|
159
|
+
"semicolon" : { errors : 1, warnings : 0 },
|
|
160
|
+
"word-list" : { errors : 1, warnings : 0 },
|
|
161
|
+
"first-person" : { errors : 0, warnings : 1 },
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("writes text with one group for each file, a summary and the counts by rule", () => {
|
|
167
|
+
const text = formatText(findings, summary);
|
|
168
|
+
expect(text).toContain("doc.md\n 1:5 error Do not use the modal verb \"should\".");
|
|
169
|
+
expect(text).toContain(" modal-verb");
|
|
170
|
+
expect(text).toContain("4 problems (3 errors, 1 warning) in 1 file of 3.");
|
|
171
|
+
expect(text).toContain("Findings by rule:");
|
|
172
|
+
expect(text).toMatch(/first-person\s+1 {2}\(1 warning\)/);
|
|
173
|
+
expect(formatText([], summarize([], 1))).toBe("No problems found in 1 file.\n");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("writes JSON with the summary and the findings", () => {
|
|
177
|
+
const json = JSON.parse(formatJson(findings, summary)) as { tool : string; summary : unknown; findings : Finding[] };
|
|
178
|
+
expect(json.tool).toBe("ste-lint");
|
|
179
|
+
expect(json.summary).toEqual(summary);
|
|
180
|
+
expect(json.findings).toHaveLength(4);
|
|
181
|
+
});
|
|
182
|
+
});
|
package/src/lint.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lint engine. It extracts the prose of a file, runs the rules on each
|
|
3
|
+
* prose unit, and gives the findings with their lines and columns.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import type { ResolvedConfig } from "./config.js";
|
|
8
|
+
import { buildLexicon, type Lexicon } from "./lexicon.js";
|
|
9
|
+
import { RULES, type Rule } from "./rules/index.js";
|
|
10
|
+
import { WordListMatcher } from "./rules/word-list-matcher.js";
|
|
11
|
+
import { LineIndex } from "./source/lines.js";
|
|
12
|
+
import { extractMarkdown, type Extraction } from "./source/markdown.js";
|
|
13
|
+
import { isSuppressed } from "./source/suppressions.js";
|
|
14
|
+
import { extractTsdoc } from "./source/tsdoc.js";
|
|
15
|
+
import { extractJsxText } from "./source/jsx.js";
|
|
16
|
+
import type { Finding, RuleId, Severity } from "./types.js";
|
|
17
|
+
import { buildUnit, type ProseUnit } from "./units.js";
|
|
18
|
+
|
|
19
|
+
export type FileKind = "markdown" | "mdx" | "typescript";
|
|
20
|
+
|
|
21
|
+
/** The kind of a file, from its extension, or null when the linter cannot read it. */
|
|
22
|
+
export function fileKind(path : string) : FileKind | null {
|
|
23
|
+
const lower = path.toLowerCase();
|
|
24
|
+
if (lower.endsWith(".mdx")) return "mdx";
|
|
25
|
+
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "markdown";
|
|
26
|
+
if (/\.(?:[mc]?ts|tsx|[mc]?js|jsx)$/.test(lower) && !lower.endsWith(".d.ts")) return "typescript";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A linter for one configuration. It makes the lexicon and the word list matcher one time. */
|
|
31
|
+
export class Linter {
|
|
32
|
+
readonly lexicon : Lexicon;
|
|
33
|
+
private readonly wordList : WordListMatcher;
|
|
34
|
+
private readonly rules : ReadonlyArray<{ readonly rule : Rule; readonly override : Severity | null }>;
|
|
35
|
+
|
|
36
|
+
constructor(readonly config : ResolvedConfig, lexicon? : Lexicon) {
|
|
37
|
+
this.lexicon = lexicon ?? buildLexicon(config);
|
|
38
|
+
this.wordList = new WordListMatcher(config.wordList);
|
|
39
|
+
this.rules = RULES.flatMap((rule) => {
|
|
40
|
+
const setting = config.rules[rule.id];
|
|
41
|
+
if (setting === "off") return [];
|
|
42
|
+
if (rule.id === "unknown-word" && this.lexicon.dictionary === null) return [];
|
|
43
|
+
return [{ rule, override : setting ?? null }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The prose units of a file. */
|
|
48
|
+
units(text : string, path : string) : ProseUnit[] {
|
|
49
|
+
return extract(text, path).blocks.flatMap((block) => buildUnit(block, this.lexicon) ?? []);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The findings in `text`. `path` gives the kind of file, and it is the file name in the findings. */
|
|
53
|
+
lintText(text : string, path : string) : Finding[] {
|
|
54
|
+
const extraction = extract(text, path);
|
|
55
|
+
const lineIndex = new LineIndex(text);
|
|
56
|
+
type Raw = { ruleId : RuleId; severity : Severity; start : number; end : number; message : string; eachWord : boolean };
|
|
57
|
+
const raw : Raw[] = [];
|
|
58
|
+
for (const block of extraction.blocks) {
|
|
59
|
+
const unit = buildUnit(block, this.lexicon);
|
|
60
|
+
if (unit === null) continue;
|
|
61
|
+
const wordOnly = unit.kind !== "paragraph";
|
|
62
|
+
for (const { rule, override } of this.rules) {
|
|
63
|
+
if (wordOnly && rule.scope !== "word") continue;
|
|
64
|
+
rule.check(unit, {
|
|
65
|
+
config : this.config,
|
|
66
|
+
lexicon : this.lexicon,
|
|
67
|
+
wordList : this.wordList,
|
|
68
|
+
report : (report) => {
|
|
69
|
+
raw.push({
|
|
70
|
+
ruleId : rule.id,
|
|
71
|
+
severity : override ?? report.severity ?? rule.defaultSeverity,
|
|
72
|
+
start : report.start,
|
|
73
|
+
end : report.end,
|
|
74
|
+
message : report.message,
|
|
75
|
+
eachWord : reportsEachWord(rule),
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// An unknown word that another rule reports already gets no second finding. A finding for a
|
|
82
|
+
// sentence, a paragraph or a noun cluster does not report each word in it.
|
|
83
|
+
const others = raw.filter((finding) => finding.ruleId !== "unknown-word" && finding.eachWord);
|
|
84
|
+
const kept = raw.filter((finding) => finding.ruleId !== "unknown-word"
|
|
85
|
+
|| !others.some((other) => other.start < finding.end && finding.start < other.end));
|
|
86
|
+
return kept
|
|
87
|
+
.filter((finding) => !isSuppressed(extraction.suppressions, finding.ruleId, finding.start))
|
|
88
|
+
.sort((a, b) => a.start - b.start || a.end - b.end || a.ruleId.localeCompare(b.ruleId))
|
|
89
|
+
.map((finding) => {
|
|
90
|
+
const start = lineIndex.position(finding.start);
|
|
91
|
+
const end = lineIndex.position(Math.max(finding.start, finding.end));
|
|
92
|
+
return {
|
|
93
|
+
file : path,
|
|
94
|
+
line : start.line,
|
|
95
|
+
column : start.column,
|
|
96
|
+
endLine : end.line,
|
|
97
|
+
endColumn : end.column,
|
|
98
|
+
ruleId : finding.ruleId,
|
|
99
|
+
severity : finding.severity,
|
|
100
|
+
message : finding.message,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The findings in a file on disk. `displayPath` is the file name in the findings. */
|
|
106
|
+
lintFile(path : string, displayPath : string = path) : Finding[] {
|
|
107
|
+
return this.lintText(readFileSync(path, "utf8"), displayPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* True when a finding of the rule is about each word in its range. A word rule reports a word or a
|
|
113
|
+
* phrase, for example a modal verb or a word list phrase. The noun-cluster rule reports a group of
|
|
114
|
+
* nouns, and the sentence and paragraph rules report a full sentence or paragraph.
|
|
115
|
+
*/
|
|
116
|
+
function reportsEachWord(rule : Rule) : boolean {
|
|
117
|
+
return rule.scope === "word" && rule.id !== "noun-cluster";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extract(text : string, path : string) : Extraction {
|
|
121
|
+
const kind = fileKind(path);
|
|
122
|
+
if (kind === "typescript") {
|
|
123
|
+
const comments = extractTsdoc(text, path);
|
|
124
|
+
if (!/\.[jt]sx$/i.test(path)) return comments;
|
|
125
|
+
// The text that the user interface shows
|
|
126
|
+
return { blocks : [...comments.blocks, ...extractJsxText(text, path).blocks], suppressions : comments.suppressions };
|
|
127
|
+
}
|
|
128
|
+
if (kind === null) return { blocks : [], suppressions : [] };
|
|
129
|
+
return extractMarkdown(text, { mdx : kind === "mdx" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The findings in `text`, with a new linter. To lint many files, make one `Linter` and use it again. */
|
|
133
|
+
export function lintText(text : string, path : string, config : ResolvedConfig, lexicon? : Lexicon) : Finding[] {
|
|
134
|
+
return new Linter(config, lexicon).lintText(text, path);
|
|
135
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isNumber, isWord } from "../text/tokens.js";
|
|
2
|
+
import { UNITS } from "../text/words.js";
|
|
3
|
+
import { forEachToken, tokenRange, type Rule } from "./rule.js";
|
|
4
|
+
|
|
5
|
+
/** Words after "about a" or "about an" that make a quantity: "about a second", "about an hour". */
|
|
6
|
+
const QUANTITY_AFTER_ARTICLE = new Set([
|
|
7
|
+
"dozen", "few", "hundred", "thousand", "million", "billion", "third", "quarter", "half", "factor", "tenth",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export const aboutQuantity : Rule = {
|
|
11
|
+
id : "about-quantity",
|
|
12
|
+
description : "Find \"about\" before a number or a quantity, where the word means \"approximately\".",
|
|
13
|
+
ste : "1.3, 9.2",
|
|
14
|
+
defaultSeverity : "error",
|
|
15
|
+
scope : "word",
|
|
16
|
+
check(unit, context) {
|
|
17
|
+
forEachToken(unit, (tokens, i) => {
|
|
18
|
+
const token = tokens[i]!;
|
|
19
|
+
if (!isWord(token) || token.lower !== "about") return;
|
|
20
|
+
const next = tokens[i + 1];
|
|
21
|
+
const after = tokens[i + 2];
|
|
22
|
+
const quantity = isNumber(next)
|
|
23
|
+
|| (next !== undefined && next.text === "~")
|
|
24
|
+
|| (isWord(next) && /^\d/.test(next.text))
|
|
25
|
+
|| (isWord(next) && (next.lower === "a" || next.lower === "an") && isWord(after) && (QUANTITY_AFTER_ARTICLE.has(after.lower) || UNITS.has(after.lower)));
|
|
26
|
+
if (!quantity) return;
|
|
27
|
+
context.report({
|
|
28
|
+
...tokenRange(tokens, i, i + 1),
|
|
29
|
+
message : "Do not use \"about\" for a quantity. Write \"approximately\". Use \"about\" only for a topic, for example \"about the monitor\".",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RuleId } from "../types.js";
|
|
2
|
+
import { aboutQuantity } from "./about-quantity.js";
|
|
3
|
+
import { ingVerb } from "./ing-verb.js";
|
|
4
|
+
import { latinAbbreviation } from "./latin-abbreviation.js";
|
|
5
|
+
import { missingSubjectRule } from "./missing-subject.js";
|
|
6
|
+
import { modalVerb } from "./modal-verb.js";
|
|
7
|
+
import { nounCluster } from "./noun-cluster.js";
|
|
8
|
+
import { paragraphLength } from "./paragraph-length.js";
|
|
9
|
+
import { passiveInstruction } from "./passive-instruction.js";
|
|
10
|
+
import { firstPerson, genderedPronoun } from "./pronouns.js";
|
|
11
|
+
import type { Rule } from "./rule.js";
|
|
12
|
+
import { semicolon } from "./semicolon.js";
|
|
13
|
+
import { sentenceLength } from "./sentence-length.js";
|
|
14
|
+
import { unknownWord } from "./unknown-word.js";
|
|
15
|
+
import { wordList } from "./word-list.js";
|
|
16
|
+
|
|
17
|
+
/** All rules, in the order of the documentation. */
|
|
18
|
+
export const RULES : readonly Rule[] = [
|
|
19
|
+
sentenceLength,
|
|
20
|
+
paragraphLength,
|
|
21
|
+
modalVerb,
|
|
22
|
+
semicolon,
|
|
23
|
+
latinAbbreviation,
|
|
24
|
+
aboutQuantity,
|
|
25
|
+
wordList,
|
|
26
|
+
genderedPronoun,
|
|
27
|
+
firstPerson,
|
|
28
|
+
passiveInstruction,
|
|
29
|
+
nounCluster,
|
|
30
|
+
missingSubjectRule,
|
|
31
|
+
ingVerb,
|
|
32
|
+
unknownWord,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** The rule with the ID `id`. */
|
|
36
|
+
export function getRule(id : RuleId) : Rule {
|
|
37
|
+
return RULES.find((rule) => rule.id === id)!;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type { Report, Rule, RuleContext, RuleScope } from "./rule.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveConfig, type SteConfigFile } from "../config.js";
|
|
3
|
+
import { Linter } from "../lint.js";
|
|
4
|
+
import { COMMON_VERBS } from "../text/words.js";
|
|
5
|
+
import { baseForm } from "./ing-verb.js";
|
|
6
|
+
|
|
7
|
+
const lint = (text : string, file : SteConfigFile = {}) =>
|
|
8
|
+
new Linter(resolveConfig(file, { root : "/" })).lintText(text, "doc.md").filter((finding) => finding.ruleId === "ing-verb");
|
|
9
|
+
const found = (text : string, file : SteConfigFile = {}) : string[] => lint(text, file).map((finding) => text.slice(finding.column - 1, finding.endColumn - 1));
|
|
10
|
+
|
|
11
|
+
describe("ing-verb", () => {
|
|
12
|
+
it("reports -ing forms used as verbs", () => {
|
|
13
|
+
expect(found("The browser is throttling timers.")).toEqual(["throttling"]);
|
|
14
|
+
expect(found("Stop it by closing the tab.")).toEqual(["closing"]);
|
|
15
|
+
expect(found("Using the worker, the monitor measures lag.")).toEqual(["Using"]);
|
|
16
|
+
expect(found("A timer firing every second is fine.")).toEqual(["firing"]);
|
|
17
|
+
expect(found("The monitor is still waiting.")).toEqual(["waiting"]);
|
|
18
|
+
const findings = lint("Stop it by closing the tab.");
|
|
19
|
+
expect(findings[0]!.severity).toBe("warning");
|
|
20
|
+
expect(findings[0]!.message).toContain("when you close");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not report nouns, adjectives or words that are not verbs", () => {
|
|
24
|
+
expect(found("The timing is good. The existing code works. Add something during the morning. The string is missing. The mark is outstanding.")).toEqual([]);
|
|
25
|
+
expect(found("Logging is fast.")).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not examine headings", () => {
|
|
29
|
+
expect(found("# Troubleshooting by restarting the worker")).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("does not report technical nouns and allowed words", () => {
|
|
33
|
+
expect(found("The page is blocking the thread.", { technicalNouns : ["blocking"] })).toEqual([]);
|
|
34
|
+
expect(found("The page is buffering the data.", { allowedWords : ["buffering"] })).toEqual([]);
|
|
35
|
+
expect(found("Read the logging pipeline docs about closing the logging pipeline.", { technicalNouns : ["logging pipeline"] })).toEqual(["closing"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("does not report a word that the word list reports", () => {
|
|
39
|
+
expect(found("The worker is running.")).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("gives the base form of a known verb for the message", () => {
|
|
43
|
+
expect(baseForm("running", COMMON_VERBS)).toBe("run");
|
|
44
|
+
expect(baseForm("using", COMMON_VERBS)).toBe("use");
|
|
45
|
+
expect(baseForm("frobnicating", COMMON_VERBS)).toBeNull();
|
|
46
|
+
expect(lint("It is frobnicating the data.")[0]!.message).not.toContain("for example");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isIngVerbUse, lemmas } from "../text/grammar.js";
|
|
2
|
+
import type { PhraseMatcher } from "../text/phrases.js";
|
|
3
|
+
import { isCounted, isWord, type Token } from "../text/tokens.js";
|
|
4
|
+
import { NON_VERB_ING } from "../text/words.js";
|
|
5
|
+
import { forEachToken, tokenRange, type Rule } from "./rule.js";
|
|
6
|
+
|
|
7
|
+
/** The token indices that a multi-word glossary phrase covers. */
|
|
8
|
+
export function coveredByPhrases(tokens : readonly Token[], phrases : PhraseMatcher) : Set<number> {
|
|
9
|
+
const covered = new Set<number>();
|
|
10
|
+
if (phrases.empty) return covered;
|
|
11
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
12
|
+
const length = phrases.longestMatch(tokens, i);
|
|
13
|
+
if (length > 1) for (let k = i; k < i + length; k++) covered.add(k);
|
|
14
|
+
}
|
|
15
|
+
return covered;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The base form of an -ing word when it is a known verb ("running" gives "run"), or null. */
|
|
19
|
+
export function baseForm(word : string, verbs : ReadonlySet<string>) : string | null {
|
|
20
|
+
return lemmas(word).find((form) => form !== word && verbs.has(form)) ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ingVerb : Rule = {
|
|
24
|
+
id : "ing-verb",
|
|
25
|
+
description : "Find -ing forms used as verbs. Headings, technical nouns and allowed words are not examined.",
|
|
26
|
+
ste : "3.2, 3.5",
|
|
27
|
+
defaultSeverity : "warning",
|
|
28
|
+
scope : "word",
|
|
29
|
+
check(unit, context) {
|
|
30
|
+
if (unit.kind === "heading") return;
|
|
31
|
+
const { lexicon, wordList } = context;
|
|
32
|
+
let covered : Set<number> = new Set();
|
|
33
|
+
let current : readonly Token[] | null = null;
|
|
34
|
+
forEachToken(unit, (tokens, i) => {
|
|
35
|
+
if (tokens !== current) {
|
|
36
|
+
current = tokens;
|
|
37
|
+
covered = coveredByPhrases(tokens, lexicon.technicalNounPhrases);
|
|
38
|
+
}
|
|
39
|
+
const token = tokens[i]!;
|
|
40
|
+
if (!isWord(token) || !isCounted(token)) return;
|
|
41
|
+
const lower = token.lower;
|
|
42
|
+
if (lower.length < 5 || !/^[a-z]+ing$/.test(lower)) return;
|
|
43
|
+
if (NON_VERB_ING.has(lower) || lexicon.technicalNouns.has(lower) || lexicon.properNouns.has(lower) || lexicon.allowedWords.has(lower)) return;
|
|
44
|
+
if (wordList.words.has(lower) || covered.has(i)) return;
|
|
45
|
+
if (!isIngVerbUse(tokens, i)) return;
|
|
46
|
+
const base = baseForm(lower, lexicon.verbs);
|
|
47
|
+
const example = base === null ? "" : `, for example "when you ${base} ..." or "to ${base} ..."`;
|
|
48
|
+
context.report({
|
|
49
|
+
...tokenRange(tokens, i, i + 1),
|
|
50
|
+
message : `Do not use "${token.text}" as a verb. Use a simple tense, the infinitive or a noun${example}. If "${lower}" is a technical noun, add it to technicalNouns.`,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isWord } from "../text/tokens.js";
|
|
2
|
+
import { forEachToken, tokenRange, type Rule } from "./rule.js";
|
|
3
|
+
|
|
4
|
+
// A map, not an object literal, so that a word such as "constructor" does not find an Object property.
|
|
5
|
+
const HELP : ReadonlyMap<string, { readonly name : string; readonly help : string }> = new Map([
|
|
6
|
+
["e.g", { name : "e.g.", help : "Write \"for example\"." }],
|
|
7
|
+
["eg", { name : "eg.", help : "Write \"for example\"." }],
|
|
8
|
+
["i.e", { name : "i.e.", help : "Write \"that is\", or write a separate sentence." }],
|
|
9
|
+
["ie", { name : "ie.", help : "Write \"that is\", or write a separate sentence." }],
|
|
10
|
+
["etc", { name : "etc.", help : "Give all the items, or write \"for example\" before the items." }],
|
|
11
|
+
["vs", { name : "vs.", help : "Write \"compared to\", \"and\" or \"or\"." }],
|
|
12
|
+
["viz", { name : "viz.", help : "Write \"that is\" or \"for example\"." }],
|
|
13
|
+
["cf", { name : "cf.", help : "Write \"compare with\" or \"refer to\"." }],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** Abbreviations that are a problem only with a period after them, because the letters alone can be another word. */
|
|
17
|
+
const NEEDS_PERIOD = new Set(["eg", "ie", "cf"]);
|
|
18
|
+
|
|
19
|
+
export const latinAbbreviation : Rule = {
|
|
20
|
+
id : "latin-abbreviation",
|
|
21
|
+
description : "Find Latin abbreviations: e.g., i.e., etc., vs., viz., cf. and et al.",
|
|
22
|
+
ste : "GR-6",
|
|
23
|
+
defaultSeverity : "error",
|
|
24
|
+
scope : "word",
|
|
25
|
+
check(unit, context) {
|
|
26
|
+
forEachToken(unit, (tokens, i) => {
|
|
27
|
+
const token = tokens[i]!;
|
|
28
|
+
if (!isWord(token)) return;
|
|
29
|
+
const next = tokens[i + 1];
|
|
30
|
+
const period = next !== undefined && next.kind === "punct" && next.text === "." && !next.spaceBefore;
|
|
31
|
+
const end = period ? i + 2 : i + 1;
|
|
32
|
+
if (token.lower === "et" && isWord(next) && next.lower === "al") {
|
|
33
|
+
const after = tokens[i + 2];
|
|
34
|
+
const withPeriod = after !== undefined && after.text === "." && !after.spaceBefore;
|
|
35
|
+
context.report({ ...tokenRange(tokens, i, withPeriod ? i + 3 : i + 2), message : "Do not use \"et al.\". Write \"and others\", or give the names." });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const entry = HELP.get(token.lower);
|
|
39
|
+
if (entry === undefined) return;
|
|
40
|
+
// "VS" in capital letters is a name (for example "VS Code"), not "versus".
|
|
41
|
+
if (token.lower === "vs" && token.text === "VS") return;
|
|
42
|
+
if (NEEDS_PERIOD.has(token.lower) && (!period || token.text !== token.lower)) return;
|
|
43
|
+
context.report({ ...tokenRange(tokens, i, end), message : `Do not use the Latin abbreviation "${entry.name}". ${entry.help}` });
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveConfig } from "../config.js";
|
|
3
|
+
import { Linter } from "../lint.js";
|
|
4
|
+
|
|
5
|
+
const linter = new Linter(resolveConfig({ technicalVerbs : ["flush"] }, { root : "/" }));
|
|
6
|
+
const lint = (comment : string, path : string = "a.ts") =>
|
|
7
|
+
linter.lintText(`${comment}\nexport const a = 1;\n`, path).filter((finding) => finding.ruleId === "missing-subject");
|
|
8
|
+
const found = (comment : string) : string[] => lint(comment).map((finding) => comment.split("\n")[finding.line - 1]!.slice(finding.column - 1, finding.endColumn - 1));
|
|
9
|
+
|
|
10
|
+
describe("missing-subject", () => {
|
|
11
|
+
it("reports a doc comment sentence that starts with a verb", () => {
|
|
12
|
+
expect(found("/** Returns the value. */")).toEqual(["Returns"]);
|
|
13
|
+
expect(found("/** Creates a monitor and starts it. */")).toEqual(["Creates"]);
|
|
14
|
+
expect(found("/** Stops `monitor` while the page is hidden. */")).toEqual(["Stops"]);
|
|
15
|
+
expect(found("/** Flushes the buffer. */")).toEqual(["Flushes"]);
|
|
16
|
+
expect(found("/** Applies the change. */")).toEqual(["Applies"]);
|
|
17
|
+
expect(found("/** Runs in the worker. */")).toEqual(["Runs"]);
|
|
18
|
+
const findings = lint("/** Returns the value. */");
|
|
19
|
+
expect(findings[0]!.severity).toBe("error");
|
|
20
|
+
expect(findings[0]!.message).toContain("This function gives");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("reports a sentence that starts with an auxiliary verb or a participle", () => {
|
|
24
|
+
expect(found("/** Can be null. */")).toEqual(["Can"]);
|
|
25
|
+
expect(found("/** Is true when the page is visible. */")).toEqual(["Is"]);
|
|
26
|
+
expect(found("/** Called when the worker stops. */")).toEqual(["Called"]);
|
|
27
|
+
expect(found("/** Used by the worker. */")).toEqual(["Used"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("examines each sentence of the summary and of @remarks", () => {
|
|
31
|
+
expect(found("/**\n * The monitor. Returns the value.\n * @remarks\n * Uses a timer.\n */")).toEqual(["Returns", "Uses"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not report a sentence with a subject, a question or a plural noun subject", () => {
|
|
35
|
+
expect(found("/** This function returns the value. */")).toEqual([]);
|
|
36
|
+
expect(found("/** Is it visible? */")).toEqual([]);
|
|
37
|
+
expect(found("/** Samples at or above this value wait for evidence. */")).toEqual([]);
|
|
38
|
+
expect(found("/** Values are clamped. */")).toEqual([]);
|
|
39
|
+
expect(found("/** Samples per calibration round. */")).toEqual([]);
|
|
40
|
+
expect(found("/** Tags whose content is code. */")).toEqual([]);
|
|
41
|
+
expect(found("/** Monitors ask whether the window overlaps. */")).toEqual([]);
|
|
42
|
+
expect(found("/** Handles returned by the setup. */")).toEqual([]);
|
|
43
|
+
expect(found("/** Settings for the run. */")).toEqual([]);
|
|
44
|
+
expect(found("/** Based on the frame time, the monitor reports lag. */")).toEqual([]);
|
|
45
|
+
expect(found("/** Status of the monitor. */")).toEqual([]);
|
|
46
|
+
expect(found("/** `stop()` releases the timers. */")).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not examine block tag text, list items or Markdown files", () => {
|
|
50
|
+
expect(found("/**\n * The function.\n * @returns Returns the value.\n * - Returns a list item.\n */")).toEqual([]);
|
|
51
|
+
expect(linter.lintText("Returns the value.", "doc.md").filter((finding) => finding.ruleId === "missing-subject")).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|