@mark1russell7/ste-lint 0.0.0-stage → 0.1.2
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 +246 -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-core.d.ts +92 -0
- package/dist/config-core.d.ts.map +1 -0
- package/dist/config-core.js +198 -0
- package/dist/config-core.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +16 -0
- package/dist/core.js.map +1 -0
- package/dist/dictionary-core.d.ts +72 -0
- package/dist/dictionary-core.d.ts.map +1 -0
- package/dist/dictionary-core.js +150 -0
- package/dist/dictionary-core.js.map +1 -0
- package/dist/dictionary.d.ts +9 -0
- package/dist/dictionary.d.ts.map +1 -0
- package/dist/dictionary.js +26 -0
- package/dist/dictionary.js.map +1 -0
- package/dist/engine.d.ts +44 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +122 -0
- package/dist/engine.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 +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -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 +18 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +23 -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/code.d.ts +9 -0
- package/dist/source/code.d.ts.map +1 -0
- package/dist/source/code.js +16 -0
- package/dist/source/code.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 +54 -3
- package/src/cli.ts +9 -0
- package/src/config-core.ts +258 -0
- package/src/config.test.ts +188 -0
- package/src/config.ts +83 -0
- package/src/core.test.ts +87 -0
- package/src/core.ts +32 -0
- package/src/dictionary-core.ts +158 -0
- package/src/dictionary.test.ts +82 -0
- package/src/dictionary.ts +26 -0
- package/src/engine.ts +138 -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 +38 -0
- package/src/lexicon.ts +92 -0
- package/src/lint.test.ts +182 -0
- package/src/lint.ts +30 -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/code.ts +17 -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/config.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configuration file `ste.config.json`, and the configuration that the
|
|
3
|
+
* linter uses after it reads the file, the environment and the defaults.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { baseConfig, CONFIG_FILE_NAME, ConfigError, DICTIONARY_ENV, validateConfig, type ResolvedConfig, type SteConfigFile } from "./config-core.js";
|
|
10
|
+
|
|
11
|
+
export * from "./config-core.js";
|
|
12
|
+
|
|
13
|
+
/** This function expands a leading `~/` to the home directory, and makes the path absolute. */
|
|
14
|
+
export function resolvePath(path : string, base : string) : string {
|
|
15
|
+
const expanded = path === "~" ? homedir() : path.startsWith("~/") || path.startsWith("~\\") ? join(homedir(), path.slice(2)) : path;
|
|
16
|
+
return resolve(base, expanded);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ResolveOptions = {
|
|
20
|
+
/** The directory of the configuration file, or the working directory. */
|
|
21
|
+
readonly root : string;
|
|
22
|
+
readonly configFile? : string | null;
|
|
23
|
+
/** The working directory. `STE_DICTIONARY` is relative to it. */
|
|
24
|
+
readonly cwd? : string;
|
|
25
|
+
readonly env? : Readonly<Record<string, string | undefined>>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** This function applies the defaults and the environment to a configuration file. */
|
|
29
|
+
export function resolveConfig(file : SteConfigFile, options : ResolveOptions) : ResolvedConfig {
|
|
30
|
+
const env = options.env ?? {};
|
|
31
|
+
const cwd = options.cwd ?? options.root;
|
|
32
|
+
const fromEnv = env[DICTIONARY_ENV];
|
|
33
|
+
let dictionaryPath : string | null = null;
|
|
34
|
+
let dictionarySource : ResolvedConfig["dictionarySource"] = null;
|
|
35
|
+
if (fromEnv !== undefined && fromEnv.trim().length > 0) {
|
|
36
|
+
dictionaryPath = resolvePath(fromEnv.trim(), cwd);
|
|
37
|
+
dictionarySource = "env";
|
|
38
|
+
} else if (typeof file.dictionaryPath === "string" && file.dictionaryPath.trim().length > 0) {
|
|
39
|
+
dictionaryPath = resolvePath(file.dictionaryPath.trim(), options.root);
|
|
40
|
+
dictionarySource = "config";
|
|
41
|
+
}
|
|
42
|
+
return { ...baseConfig(file, { root : options.root, configFile : options.configFile ?? null }), dictionaryPath, dictionarySource };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The first `ste.config.json` in `cwd` or one of its parent directories, or null. */
|
|
46
|
+
export function findConfigFile(cwd : string) : string | null {
|
|
47
|
+
let directory = resolve(cwd);
|
|
48
|
+
for (;;) {
|
|
49
|
+
const candidate = join(directory, CONFIG_FILE_NAME);
|
|
50
|
+
if (existsSync(candidate)) return candidate;
|
|
51
|
+
const parent = dirname(directory);
|
|
52
|
+
if (parent === directory) return null;
|
|
53
|
+
directory = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type LoadOptions = {
|
|
58
|
+
readonly cwd : string;
|
|
59
|
+
/** An explicit configuration file. Without it, the linter looks for `ste.config.json`. */
|
|
60
|
+
readonly configPath? : string;
|
|
61
|
+
readonly env? : Readonly<Record<string, string | undefined>>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** This function reads the configuration file, if there is one, and resolves it. */
|
|
65
|
+
export function loadConfig(options : LoadOptions) : ResolvedConfig {
|
|
66
|
+
const configFile = options.configPath !== undefined ? resolve(options.cwd, options.configPath) : findConfigFile(options.cwd);
|
|
67
|
+
const env = options.env ?? {};
|
|
68
|
+
if (configFile === null) return resolveConfig({}, { root : resolve(options.cwd), cwd : options.cwd, env });
|
|
69
|
+
let text : string;
|
|
70
|
+
try {
|
|
71
|
+
text = readFileSync(configFile, "utf8");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new ConfigError(`Cannot read the configuration file ${configFile}: ${(error as Error).message}`);
|
|
74
|
+
}
|
|
75
|
+
let value : unknown;
|
|
76
|
+
try {
|
|
77
|
+
value = JSON.parse(text);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new ConfigError(`${configFile}: the file is not valid JSON: ${(error as Error).message}`);
|
|
80
|
+
}
|
|
81
|
+
const file = validateConfig(value, configFile);
|
|
82
|
+
return resolveConfig(file, { root : dirname(configFile), configFile, cwd : options.cwd, env });
|
|
83
|
+
}
|
package/src/core.test.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import * as core from "./core.js";
|
|
6
|
+
import { extractCode } from "./source/code.js";
|
|
7
|
+
import { Linter, resolveConfig } from "./index.js";
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
/** The relative imports and the package imports of each module that `entry` reaches. */
|
|
12
|
+
function importGraph(entry : string) : { modules : string[]; packages : string[] } {
|
|
13
|
+
const seen = new Set<string>();
|
|
14
|
+
const packages = new Set<string>();
|
|
15
|
+
const visit = (file : string) : void => {
|
|
16
|
+
if (seen.has(file)) return;
|
|
17
|
+
seen.add(file);
|
|
18
|
+
const text = readFileSync(file, "utf8");
|
|
19
|
+
// Type-only imports are erased, so they do not count.
|
|
20
|
+
for (const match of text.matchAll(/^(?:import|export)\s+(?!type\b)[^;]*?from\s+"([^"]+)"/gms)) {
|
|
21
|
+
const specifier = match[1]!;
|
|
22
|
+
if (specifier.startsWith(".")) visit(join(dirname(file), specifier.replace(/\.js$/, ".ts")));
|
|
23
|
+
else packages.add(specifier);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
visit(join(here, entry));
|
|
27
|
+
return { modules : [...seen], packages : [...packages] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FIXTURE = [
|
|
31
|
+
"# Getting started",
|
|
32
|
+
"",
|
|
33
|
+
"You should utilize the CLI in order to check the docs; it is basically simple.",
|
|
34
|
+
"The values are about 5 ms, e.g. in the test run.",
|
|
35
|
+
"",
|
|
36
|
+
"Please make sure that the file is deleted. Stop the tool by closing the tab.",
|
|
37
|
+
"",
|
|
38
|
+
"- The user can edit his settings via the menu.",
|
|
39
|
+
"",
|
|
40
|
+
"<!-- ste-disable-next modal-verb -->",
|
|
41
|
+
"You should not see this modal verb.",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n");
|
|
44
|
+
|
|
45
|
+
describe("the core entry", () => {
|
|
46
|
+
it("imports no Node module and not the TypeScript compiler", () => {
|
|
47
|
+
const { packages, modules } = importGraph("core.ts");
|
|
48
|
+
expect(packages).toEqual([]);
|
|
49
|
+
expect(modules.length).toBeGreaterThan(20);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("lets the code extractor import the compiler, but no Node module", () => {
|
|
53
|
+
expect(importGraph("source/code.ts").packages).toEqual(["typescript"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("gives the same Markdown findings as the linter of the command line", () => {
|
|
57
|
+
const config = resolveConfig({}, { root : "/" });
|
|
58
|
+
const cli = new Linter(config).lintText(FIXTURE, "README.md");
|
|
59
|
+
const browser = new core.ProseLinter(core.baseConfig()).lintText(FIXTURE, "README.md");
|
|
60
|
+
expect(browser).toEqual(cli);
|
|
61
|
+
expect(new Set(cli.map((finding) => finding.ruleId))).toEqual(new Set([
|
|
62
|
+
"modal-verb", "semicolon", "word-list", "about-quantity", "latin-abbreviation", "gendered-pronoun", "ing-verb",
|
|
63
|
+
]));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("gives the same doc comment findings with the code extractor", () => {
|
|
67
|
+
const source = "/** Returns the value. You should utilize it. */\nexport const x = 1;\n";
|
|
68
|
+
const cli = new Linter(resolveConfig({}, { root : "/" })).lintText(source, "a.ts");
|
|
69
|
+
const browser = new core.ProseLinter(core.baseConfig(), undefined, { extractCode }).lintText(source, "a.ts");
|
|
70
|
+
expect(browser).toEqual(cli);
|
|
71
|
+
expect(cli.map((finding) => finding.ruleId)).toContain("missing-subject");
|
|
72
|
+
expect(new core.ProseLinter(core.baseConfig()).lintText(source, "a.ts")).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("applies the settings of a configuration file and a dictionary", () => {
|
|
76
|
+
const config = core.baseConfig({ rules : { "modal-verb" : "off" } });
|
|
77
|
+
expect(new core.ProseLinter(config).lintText("You should stop.", "a.md")).toEqual([]);
|
|
78
|
+
const dictionary = core.parseDictionary(["you", "stop", "the"], "words.json");
|
|
79
|
+
const linter = new core.ProseLinter(core.baseConfig(), core.buildLexicon(core.baseConfig(), dictionary));
|
|
80
|
+
expect(linter.lintText("You stop the valve.", "a.md").map((finding) => finding.ruleId)).toEqual(["unknown-word"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("gives the same configuration as resolveConfig without a dictionary", () => {
|
|
84
|
+
const file = { technicalNouns : ["valve"], limits : { instructionWords : 12 } };
|
|
85
|
+
expect(core.baseConfig(file, { root : "/" })).toEqual(resolveConfig(file, { root : "/" }));
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mark1russell7/ste-lint/core`: the linter without a file system, for a
|
|
3
|
+
* browser or another host that has the text in memory. It examines Markdown
|
|
4
|
+
* and MDX. For TypeScript doc comments, give the extractor of
|
|
5
|
+
* `@mark1russell7/ste-lint/code` to `ProseLinter`. The rules are the same
|
|
6
|
+
* rules that the command line uses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
baseConfig,
|
|
11
|
+
CONFIG_FILE_NAME,
|
|
12
|
+
ConfigError,
|
|
13
|
+
DEFAULT_IGNORE,
|
|
14
|
+
DEFAULT_INCLUDE,
|
|
15
|
+
DEFAULT_LIMITS,
|
|
16
|
+
DICTIONARY_ENV,
|
|
17
|
+
mergeWordList,
|
|
18
|
+
validateConfig,
|
|
19
|
+
type BaseConfigOptions,
|
|
20
|
+
type Limits,
|
|
21
|
+
type LimitsConfig,
|
|
22
|
+
type ResolvedConfig,
|
|
23
|
+
type SteConfigFile,
|
|
24
|
+
type WordListEntryConfig,
|
|
25
|
+
} from "./config-core.js";
|
|
26
|
+
export { Dictionary, DICTIONARY_FORMAT, DictionaryError, parseDictionary, type DictionaryEntry, type DictionaryLookup } from "./dictionary-core.js";
|
|
27
|
+
export { fileKind, ProseLinter, type CodeExtractor, type FileKind, type ProseLinterOptions } from "./engine.js";
|
|
28
|
+
export { formatJson, formatText, summarize, type RuleCount, type Summary } from "./format.js";
|
|
29
|
+
export { buildLexicon, type Lexicon } from "./lexicon.js";
|
|
30
|
+
export { getRule, RULES, type Rule } from "./rules/index.js";
|
|
31
|
+
export { isRuleId, RULE_IDS, type Finding, type RuleId, type RuleSetting, type Severity } from "./types.js";
|
|
32
|
+
export { DEFAULT_WORD_LIST, type WordListEntry } from "./word-list.js";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional local dictionary of approved words.
|
|
3
|
+
*
|
|
4
|
+
* The ASD-STE100 dictionary is copyrighted. This package does not contain
|
|
5
|
+
* it, and the repository must not contain it. Each user makes a local file
|
|
6
|
+
* from their own copy of the standard. Keep that file outside the
|
|
7
|
+
* repository, or in a git-ignored folder, for example `.ste/`.
|
|
8
|
+
*
|
|
9
|
+
* File format (JSON). The short form is an array of approved words:
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* ["a", "about", "make", "makes", "made"]
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The full form is an object with entries:
|
|
16
|
+
*
|
|
17
|
+
* ```json
|
|
18
|
+
* {
|
|
19
|
+
* "format": "ste-lint-dictionary",
|
|
20
|
+
* "version": 1,
|
|
21
|
+
* "entries": [
|
|
22
|
+
* { "word": "make", "pos": "verb", "forms": ["makes", "made"] },
|
|
23
|
+
* { "word": "valve", "pos": "noun" },
|
|
24
|
+
* { "word": "utilize", "pos": "verb", "approved": false, "alternatives": ["use"] }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* - `word` (necessary): the headword. A phrase, for example "make sure", approves each of its words.
|
|
30
|
+
* - `pos`: the part of speech. A noun also permits its regular plural.
|
|
31
|
+
* - `approved`: `false` for a word that is not approved. The default is `true`.
|
|
32
|
+
* - `forms`: other approved forms, for example the past tense of a verb.
|
|
33
|
+
* - `alternatives`: approved words to use instead of a word that is not approved.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export type DictionaryEntry = {
|
|
37
|
+
readonly word : string;
|
|
38
|
+
readonly pos? : string;
|
|
39
|
+
readonly approved? : boolean;
|
|
40
|
+
readonly forms? : readonly string[];
|
|
41
|
+
readonly alternatives? : readonly string[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type DictionaryLookup =
|
|
45
|
+
| { readonly status : "approved" }
|
|
46
|
+
| { readonly status : "not-approved"; readonly alternatives : readonly string[] }
|
|
47
|
+
| { readonly status : "unknown" };
|
|
48
|
+
|
|
49
|
+
export const DICTIONARY_FORMAT = "ste-lint-dictionary";
|
|
50
|
+
|
|
51
|
+
/** An error in a dictionary file. */
|
|
52
|
+
export class DictionaryError extends Error {
|
|
53
|
+
override readonly name : string = "DictionaryError";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalize = (word : string) : string => word.trim().toLowerCase().replace(/[’‘]/g, "'");
|
|
57
|
+
|
|
58
|
+
/** The approved words of a local dictionary. */
|
|
59
|
+
export class Dictionary {
|
|
60
|
+
private readonly approved = new Set<string>();
|
|
61
|
+
private readonly approvedNouns = new Set<string>();
|
|
62
|
+
private readonly notApproved = new Map<string, readonly string[]>();
|
|
63
|
+
/** The approved nouns. The noun-cluster rule uses them. */
|
|
64
|
+
readonly nouns : Set<string> = new Set<string>();
|
|
65
|
+
/** The approved verbs. The imperative and missing-subject heuristics use them. */
|
|
66
|
+
readonly verbs : Set<string> = new Set<string>();
|
|
67
|
+
|
|
68
|
+
constructor(entries : readonly DictionaryEntry[]) {
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const words = [entry.word, ...(entry.forms ?? [])].flatMap((form) => normalize(form).split(/\s+/)).filter((form) => form.length > 0);
|
|
71
|
+
if (entry.approved === false) {
|
|
72
|
+
for (const word of words) this.notApproved.set(word, entry.alternatives ?? []);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const word of words) this.approved.add(word);
|
|
76
|
+
const pos = (entry.pos ?? "").toLowerCase();
|
|
77
|
+
const base = normalize(entry.word);
|
|
78
|
+
if (pos === "n" || pos === "noun") {
|
|
79
|
+
this.approvedNouns.add(base);
|
|
80
|
+
this.nouns.add(base);
|
|
81
|
+
}
|
|
82
|
+
if (pos === "v" || pos === "verb") this.verbs.add(base);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The number of approved words and forms. */
|
|
87
|
+
get size() : number {
|
|
88
|
+
return this.approved.size;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The status of `word` in the dictionary. */
|
|
92
|
+
lookup(word : string) : DictionaryLookup {
|
|
93
|
+
const lower = normalize(word);
|
|
94
|
+
const candidates = [lower];
|
|
95
|
+
if (lower.endsWith("'s")) candidates.push(lower.slice(0, -2));
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
if (this.approved.has(candidate)) return { status : "approved" };
|
|
98
|
+
const plurals = [candidate.slice(0, -1), candidate.slice(0, -2), candidate.slice(0, -3) + "y"];
|
|
99
|
+
if (candidate.endsWith("s") && plurals.some((singular) => this.approvedNouns.has(singular))) return { status : "approved" };
|
|
100
|
+
}
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
const alternatives = this.notApproved.get(candidate);
|
|
103
|
+
if (alternatives !== undefined) return { status : "not-approved", alternatives };
|
|
104
|
+
}
|
|
105
|
+
return { status : "unknown" };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** This function makes a dictionary from parsed JSON. `source` is the file name for error messages. */
|
|
110
|
+
export function parseDictionary(value : unknown, source : string) : Dictionary {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
if (!value.every((word) => typeof word === "string")) {
|
|
113
|
+
throw new DictionaryError(`${source}: the array form must contain only strings.`);
|
|
114
|
+
}
|
|
115
|
+
return new Dictionary(value.map((word : string) => ({ word })));
|
|
116
|
+
}
|
|
117
|
+
if (typeof value !== "object" || value === null) {
|
|
118
|
+
throw new DictionaryError(`${source}: the file must contain an array of words or an object with "entries".`);
|
|
119
|
+
}
|
|
120
|
+
const record = value as Record<string, unknown>;
|
|
121
|
+
const format = record["format"];
|
|
122
|
+
if (format !== undefined && format !== DICTIONARY_FORMAT) {
|
|
123
|
+
throw new DictionaryError(`${source}: "format" must be "${DICTIONARY_FORMAT}".`);
|
|
124
|
+
}
|
|
125
|
+
const entries = record["entries"];
|
|
126
|
+
if (!Array.isArray(entries)) throw new DictionaryError(`${source}: "entries" must be an array.`);
|
|
127
|
+
const parsed : DictionaryEntry[] = entries.map((entry : unknown, index : number) => parseEntry(entry, `${source}: entries[${index}]`));
|
|
128
|
+
return new Dictionary(parsed);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function stringArray(value : unknown, where : string) : string[] | undefined {
|
|
132
|
+
if (value === undefined) return undefined;
|
|
133
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
134
|
+
throw new DictionaryError(`${where} must be an array of strings.`);
|
|
135
|
+
}
|
|
136
|
+
return value as string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseEntry(entry : unknown, where : string) : DictionaryEntry {
|
|
140
|
+
if (typeof entry === "string") return { word : entry };
|
|
141
|
+
if (typeof entry !== "object" || entry === null) throw new DictionaryError(`${where} must be a string or an object.`);
|
|
142
|
+
const record = entry as Record<string, unknown>;
|
|
143
|
+
const word = record["word"];
|
|
144
|
+
if (typeof word !== "string" || word.trim().length === 0) throw new DictionaryError(`${where}.word must be a string that is not empty.`);
|
|
145
|
+
const pos = record["pos"];
|
|
146
|
+
if (pos !== undefined && typeof pos !== "string") throw new DictionaryError(`${where}.pos must be a string.`);
|
|
147
|
+
const approved = record["approved"];
|
|
148
|
+
if (approved !== undefined && typeof approved !== "boolean") throw new DictionaryError(`${where}.approved must be true or false.`);
|
|
149
|
+
const forms = stringArray(record["forms"], `${where}.forms`);
|
|
150
|
+
const alternatives = stringArray(record["alternatives"], `${where}.alternatives`);
|
|
151
|
+
return {
|
|
152
|
+
word,
|
|
153
|
+
...(pos === undefined ? {} : { pos }),
|
|
154
|
+
...(approved === undefined ? {} : { approved }),
|
|
155
|
+
...(forms === undefined ? {} : { forms }),
|
|
156
|
+
...(alternatives === undefined ? {} : { alternatives }),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { 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 } from "vitest";
|
|
5
|
+
import { Dictionary, DictionaryError, loadDictionary, parseDictionary } from "./dictionary.js";
|
|
6
|
+
|
|
7
|
+
let directory : string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
directory = mkdtempSync(join(tmpdir(), "ste-dictionary-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(directory, { recursive : true, force : true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("parseDictionary", () => {
|
|
18
|
+
it("reads the short form: an array of approved words", () => {
|
|
19
|
+
const dictionary = parseDictionary(["Make", "makes", "valve"], "d.json");
|
|
20
|
+
expect(dictionary.size).toBe(3);
|
|
21
|
+
expect(dictionary.lookup("make").status).toBe("approved");
|
|
22
|
+
expect(dictionary.lookup("MAKES").status).toBe("approved");
|
|
23
|
+
expect(dictionary.lookup("made").status).toBe("unknown");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("reads the full form with parts of speech, forms and words that are not approved", () => {
|
|
27
|
+
const dictionary = parseDictionary({
|
|
28
|
+
format : "ste-lint-dictionary",
|
|
29
|
+
version : 1,
|
|
30
|
+
entries : [
|
|
31
|
+
"a",
|
|
32
|
+
{ word : "make", pos : "verb", forms : ["makes", "made"] },
|
|
33
|
+
{ word : "make sure", pos : "verb" },
|
|
34
|
+
{ word : "valve", pos : "noun" },
|
|
35
|
+
{ word : "battery", pos : "n" },
|
|
36
|
+
{ word : "utilize", pos : "v", approved : false, alternatives : ["use"] },
|
|
37
|
+
{ word : "frob", approved : false },
|
|
38
|
+
],
|
|
39
|
+
}, "d.json");
|
|
40
|
+
expect(dictionary.lookup("made").status).toBe("approved");
|
|
41
|
+
expect(dictionary.lookup("sure").status).toBe("approved");
|
|
42
|
+
expect(dictionary.lookup("valves").status).toBe("approved");
|
|
43
|
+
expect(dictionary.lookup("batteries").status).toBe("approved");
|
|
44
|
+
expect(dictionary.lookup("valve's").status).toBe("approved");
|
|
45
|
+
expect(dictionary.lookup("makings").status).toBe("unknown");
|
|
46
|
+
expect(dictionary.lookup("Utilize")).toEqual({ status : "not-approved", alternatives : ["use"] });
|
|
47
|
+
expect(dictionary.lookup("frob")).toEqual({ status : "not-approved", alternatives : [] });
|
|
48
|
+
expect([...dictionary.nouns].sort()).toEqual(["battery", "valve"]);
|
|
49
|
+
expect([...dictionary.verbs].sort()).toEqual(["make", "make sure"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("gives clear errors for a file that is not valid", () => {
|
|
53
|
+
expect(() => parseDictionary(["a", 1], "d.json")).toThrow("only strings");
|
|
54
|
+
expect(() => parseDictionary(3, "d.json")).toThrow(DictionaryError);
|
|
55
|
+
expect(() => parseDictionary({ format : "other", entries : [] }, "d.json")).toThrow("\"format\" must be");
|
|
56
|
+
expect(() => parseDictionary({ entries : {} }, "d.json")).toThrow("\"entries\" must be an array");
|
|
57
|
+
expect(() => parseDictionary({ entries : [5] }, "d.json")).toThrow("entries[0] must be a string or an object");
|
|
58
|
+
expect(() => parseDictionary({ entries : [{ word : "" }] }, "d.json")).toThrow("word must be a string");
|
|
59
|
+
expect(() => parseDictionary({ entries : [{ word : "a", pos : 1 }] }, "d.json")).toThrow("pos must be a string");
|
|
60
|
+
expect(() => parseDictionary({ entries : [{ word : "a", approved : "no" }] }, "d.json")).toThrow("approved must be true or false");
|
|
61
|
+
expect(() => parseDictionary({ entries : [{ word : "a", forms : "b" }] }, "d.json")).toThrow("forms must be an array of strings");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("makes an empty dictionary", () => {
|
|
65
|
+
expect(new Dictionary([]).lookup("anything").status).toBe("unknown");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("loadDictionary", () => {
|
|
70
|
+
it("reads a JSON file", () => {
|
|
71
|
+
const path = join(directory, "dictionary.json");
|
|
72
|
+
writeFileSync(path, JSON.stringify(["valve"]));
|
|
73
|
+
expect(loadDictionary(path).lookup("valve").status).toBe("approved");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gives clear errors for a missing file and a file that is not JSON", () => {
|
|
77
|
+
expect(() => loadDictionary(join(directory, "missing.json"))).toThrow("Cannot read the dictionary file");
|
|
78
|
+
const path = join(directory, "broken.json");
|
|
79
|
+
writeFileSync(path, "[");
|
|
80
|
+
expect(() => loadDictionary(path)).toThrow("is not valid JSON");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local dictionary file. The format and the lookup are in
|
|
3
|
+
* `dictionary-core.ts`, which a browser can use. This module reads the file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { DictionaryError, parseDictionary, type Dictionary } from "./dictionary-core.js";
|
|
8
|
+
|
|
9
|
+
export * from "./dictionary-core.js";
|
|
10
|
+
|
|
11
|
+
/** This function reads a dictionary file. */
|
|
12
|
+
export function loadDictionary(path : string) : Dictionary {
|
|
13
|
+
let text : string;
|
|
14
|
+
try {
|
|
15
|
+
text = readFileSync(path, "utf8");
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new DictionaryError(`Cannot read the dictionary file ${path}: ${(error as Error).message}`);
|
|
18
|
+
}
|
|
19
|
+
let value : unknown;
|
|
20
|
+
try {
|
|
21
|
+
value = JSON.parse(text);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new DictionaryError(`${path}: the file is not valid JSON: ${(error as Error).message}`);
|
|
24
|
+
}
|
|
25
|
+
return parseDictionary(value, path);
|
|
26
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lint engine without a file system. It extracts the prose of a text,
|
|
3
|
+
* runs the rules on each prose unit, and gives the findings with their lines
|
|
4
|
+
* and columns. A browser can use this module.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ResolvedConfig } from "./config-core.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 type { Finding, RuleId, Severity } from "./types.js";
|
|
15
|
+
import { buildUnit, type ProseUnit } from "./units.js";
|
|
16
|
+
|
|
17
|
+
export type FileKind = "markdown" | "mdx" | "typescript";
|
|
18
|
+
|
|
19
|
+
/** The kind of a file, from its extension, or null when the linter cannot read it. */
|
|
20
|
+
export function fileKind(path : string) : FileKind | null {
|
|
21
|
+
const lower = path.toLowerCase();
|
|
22
|
+
if (lower.endsWith(".mdx")) return "mdx";
|
|
23
|
+
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "markdown";
|
|
24
|
+
if (/\.(?:[mc]?ts|tsx|[mc]?js|jsx)$/.test(lower) && !lower.endsWith(".d.ts")) return "typescript";
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A function that extracts the prose of a TypeScript or JavaScript file:
|
|
30
|
+
* the doc comments, and the JSX text of a TSX or JSX file.
|
|
31
|
+
*/
|
|
32
|
+
export type CodeExtractor = (text : string, path : string) => Extraction;
|
|
33
|
+
|
|
34
|
+
export type ProseLinterOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* The extractor for TypeScript and JavaScript files. Without it, the
|
|
37
|
+
* linter gives no findings for these files. The extractor of this
|
|
38
|
+
* package (`extractCode`) needs the `typescript` package.
|
|
39
|
+
*/
|
|
40
|
+
readonly extractCode? : CodeExtractor;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A linter for one configuration, for text in memory. It makes the lexicon
|
|
45
|
+
* and the word list matcher one time. It reads no file.
|
|
46
|
+
*/
|
|
47
|
+
export class ProseLinter {
|
|
48
|
+
readonly lexicon : Lexicon;
|
|
49
|
+
private readonly wordList : WordListMatcher;
|
|
50
|
+
private readonly rules : ReadonlyArray<{ readonly rule : Rule; readonly override : Severity | null }>;
|
|
51
|
+
private readonly extractCode : CodeExtractor | null;
|
|
52
|
+
|
|
53
|
+
constructor(readonly config : ResolvedConfig, lexicon? : Lexicon, options : ProseLinterOptions = {}) {
|
|
54
|
+
this.lexicon = lexicon ?? buildLexicon(config);
|
|
55
|
+
this.wordList = new WordListMatcher(config.wordList);
|
|
56
|
+
this.extractCode = options.extractCode ?? null;
|
|
57
|
+
this.rules = RULES.flatMap((rule) => {
|
|
58
|
+
const setting = config.rules[rule.id];
|
|
59
|
+
if (setting === "off") return [];
|
|
60
|
+
if (rule.id === "unknown-word" && this.lexicon.dictionary === null) return [];
|
|
61
|
+
return [{ rule, override : setting ?? null }];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The prose units of a file. */
|
|
66
|
+
units(text : string, path : string) : ProseUnit[] {
|
|
67
|
+
return this.extract(text, path).blocks.flatMap((block) => buildUnit(block, this.lexicon) ?? []);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The findings in `text`. `path` gives the kind of file, and it is the file name in the findings. */
|
|
71
|
+
lintText(text : string, path : string) : Finding[] {
|
|
72
|
+
const extraction = this.extract(text, path);
|
|
73
|
+
const lineIndex = new LineIndex(text);
|
|
74
|
+
type Raw = { ruleId : RuleId; severity : Severity; start : number; end : number; message : string; eachWord : boolean };
|
|
75
|
+
const raw : Raw[] = [];
|
|
76
|
+
for (const block of extraction.blocks) {
|
|
77
|
+
const unit = buildUnit(block, this.lexicon);
|
|
78
|
+
if (unit === null) continue;
|
|
79
|
+
const wordOnly = unit.kind !== "paragraph";
|
|
80
|
+
for (const { rule, override } of this.rules) {
|
|
81
|
+
if (wordOnly && rule.scope !== "word") continue;
|
|
82
|
+
rule.check(unit, {
|
|
83
|
+
config : this.config,
|
|
84
|
+
lexicon : this.lexicon,
|
|
85
|
+
wordList : this.wordList,
|
|
86
|
+
report : (report) => {
|
|
87
|
+
raw.push({
|
|
88
|
+
ruleId : rule.id,
|
|
89
|
+
severity : override ?? report.severity ?? rule.defaultSeverity,
|
|
90
|
+
start : report.start,
|
|
91
|
+
end : report.end,
|
|
92
|
+
message : report.message,
|
|
93
|
+
eachWord : reportsEachWord(rule),
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// An unknown word that another rule reports already gets no second finding. A finding for a
|
|
100
|
+
// sentence, a paragraph or a noun cluster does not report each word in it.
|
|
101
|
+
const others = raw.filter((finding) => finding.ruleId !== "unknown-word" && finding.eachWord);
|
|
102
|
+
const kept = raw.filter((finding) => finding.ruleId !== "unknown-word"
|
|
103
|
+
|| !others.some((other) => other.start < finding.end && finding.start < other.end));
|
|
104
|
+
return kept
|
|
105
|
+
.filter((finding) => !isSuppressed(extraction.suppressions, finding.ruleId, finding.start))
|
|
106
|
+
.sort((a, b) => a.start - b.start || a.end - b.end || a.ruleId.localeCompare(b.ruleId))
|
|
107
|
+
.map((finding) => {
|
|
108
|
+
const start = lineIndex.position(finding.start);
|
|
109
|
+
const end = lineIndex.position(Math.max(finding.start, finding.end));
|
|
110
|
+
return {
|
|
111
|
+
file : path,
|
|
112
|
+
line : start.line,
|
|
113
|
+
column : start.column,
|
|
114
|
+
endLine : end.line,
|
|
115
|
+
endColumn : end.column,
|
|
116
|
+
ruleId : finding.ruleId,
|
|
117
|
+
severity : finding.severity,
|
|
118
|
+
message : finding.message,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private extract(text : string, path : string) : Extraction {
|
|
124
|
+
const kind = fileKind(path);
|
|
125
|
+
if (kind === "typescript") return this.extractCode === null ? { blocks : [], suppressions : [] } : this.extractCode(text, path);
|
|
126
|
+
if (kind === null) return { blocks : [], suppressions : [] };
|
|
127
|
+
return extractMarkdown(text, { mdx : kind === "mdx" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* True when a finding of the rule is about each word in its range. A word rule reports a word or a
|
|
133
|
+
* phrase, for example a modal verb or a word list phrase. The noun-cluster rule reports a group of
|
|
134
|
+
* nouns, and the sentence and paragraph rules report a full sentence or paragraph.
|
|
135
|
+
*/
|
|
136
|
+
function reportsEachWord(rule : Rule) : boolean {
|
|
137
|
+
return rule.scope === "word" && rule.id !== "noun-cluster";
|
|
138
|
+
}
|