@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.
Files changed (253) hide show
  1. package/README.md +246 -2
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +9 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config-core.d.ts +92 -0
  8. package/dist/config-core.d.ts.map +1 -0
  9. package/dist/config-core.js +198 -0
  10. package/dist/config-core.js.map +1 -0
  11. package/dist/config.d.ts +29 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +68 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core.d.ts +16 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +16 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/dictionary-core.d.ts +72 -0
  20. package/dist/dictionary-core.d.ts.map +1 -0
  21. package/dist/dictionary-core.js +150 -0
  22. package/dist/dictionary-core.js.map +1 -0
  23. package/dist/dictionary.d.ts +9 -0
  24. package/dist/dictionary.d.ts.map +1 -0
  25. package/dist/dictionary.js +26 -0
  26. package/dist/dictionary.js.map +1 -0
  27. package/dist/engine.d.ts +44 -0
  28. package/dist/engine.d.ts.map +1 -0
  29. package/dist/engine.js +122 -0
  30. package/dist/engine.js.map +1 -0
  31. package/dist/files.d.ts +35 -0
  32. package/dist/files.d.ts.map +1 -0
  33. package/dist/files.js +181 -0
  34. package/dist/files.js.map +1 -0
  35. package/dist/format.d.ts +21 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +61 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +19 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +19 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/lexicon.d.ts +33 -0
  44. package/dist/lexicon.d.ts.map +1 -0
  45. package/dist/lexicon.js +71 -0
  46. package/dist/lexicon.js.map +1 -0
  47. package/dist/lint.d.ts +18 -0
  48. package/dist/lint.d.ts.map +1 -0
  49. package/dist/lint.js +23 -0
  50. package/dist/lint.js.map +1 -0
  51. package/dist/rules/about-quantity.d.ts +3 -0
  52. package/dist/rules/about-quantity.d.ts.map +1 -0
  53. package/dist/rules/about-quantity.js +34 -0
  54. package/dist/rules/about-quantity.js.map +1 -0
  55. package/dist/rules/index.d.ts +8 -0
  56. package/dist/rules/index.d.ts.map +1 -0
  57. package/dist/rules/index.js +35 -0
  58. package/dist/rules/index.js.map +1 -0
  59. package/dist/rules/ing-verb.d.ts +9 -0
  60. package/dist/rules/ing-verb.d.ts.map +1 -0
  61. package/dist/rules/ing-verb.js +60 -0
  62. package/dist/rules/ing-verb.js.map +1 -0
  63. package/dist/rules/latin-abbreviation.d.ts +3 -0
  64. package/dist/rules/latin-abbreviation.d.ts.map +1 -0
  65. package/dist/rules/latin-abbreviation.js +48 -0
  66. package/dist/rules/latin-abbreviation.js.map +1 -0
  67. package/dist/rules/missing-subject.d.ts +16 -0
  68. package/dist/rules/missing-subject.d.ts.map +1 -0
  69. package/dist/rules/missing-subject.js +138 -0
  70. package/dist/rules/missing-subject.js.map +1 -0
  71. package/dist/rules/modal-verb.d.ts +3 -0
  72. package/dist/rules/modal-verb.d.ts.map +1 -0
  73. package/dist/rules/modal-verb.js +72 -0
  74. package/dist/rules/modal-verb.js.map +1 -0
  75. package/dist/rules/noun-cluster.d.ts +17 -0
  76. package/dist/rules/noun-cluster.d.ts.map +1 -0
  77. package/dist/rules/noun-cluster.js +83 -0
  78. package/dist/rules/noun-cluster.js.map +1 -0
  79. package/dist/rules/paragraph-length.d.ts +3 -0
  80. package/dist/rules/paragraph-length.d.ts.map +1 -0
  81. package/dist/rules/paragraph-length.js +19 -0
  82. package/dist/rules/paragraph-length.js.map +1 -0
  83. package/dist/rules/passive-instruction.d.ts +15 -0
  84. package/dist/rules/passive-instruction.d.ts.map +1 -0
  85. package/dist/rules/passive-instruction.js +38 -0
  86. package/dist/rules/passive-instruction.js.map +1 -0
  87. package/dist/rules/pronouns.d.ts +4 -0
  88. package/dist/rules/pronouns.d.ts.map +1 -0
  89. package/dist/rules/pronouns.js +57 -0
  90. package/dist/rules/pronouns.js.map +1 -0
  91. package/dist/rules/rule.d.ts +46 -0
  92. package/dist/rules/rule.d.ts.map +1 -0
  93. package/dist/rules/rule.js +31 -0
  94. package/dist/rules/rule.js.map +1 -0
  95. package/dist/rules/semicolon.d.ts +3 -0
  96. package/dist/rules/semicolon.d.ts.map +1 -0
  97. package/dist/rules/semicolon.js +20 -0
  98. package/dist/rules/semicolon.js.map +1 -0
  99. package/dist/rules/sentence-length.d.ts +3 -0
  100. package/dist/rules/sentence-length.d.ts.map +1 -0
  101. package/dist/rules/sentence-length.js +31 -0
  102. package/dist/rules/sentence-length.js.map +1 -0
  103. package/dist/rules/unknown-word.d.ts +9 -0
  104. package/dist/rules/unknown-word.d.ts.map +1 -0
  105. package/dist/rules/unknown-word.js +49 -0
  106. package/dist/rules/unknown-word.js.map +1 -0
  107. package/dist/rules/word-list-matcher.d.ts +25 -0
  108. package/dist/rules/word-list-matcher.d.ts.map +1 -0
  109. package/dist/rules/word-list-matcher.js +60 -0
  110. package/dist/rules/word-list-matcher.js.map +1 -0
  111. package/dist/rules/word-list.d.ts +3 -0
  112. package/dist/rules/word-list.d.ts.map +1 -0
  113. package/dist/rules/word-list.js +26 -0
  114. package/dist/rules/word-list.js.map +1 -0
  115. package/dist/run-cli.d.ts +27 -0
  116. package/dist/run-cli.d.ts.map +1 -0
  117. package/dist/run-cli.js +176 -0
  118. package/dist/run-cli.js.map +1 -0
  119. package/dist/source/blocks.d.ts +55 -0
  120. package/dist/source/blocks.d.ts.map +1 -0
  121. package/dist/source/blocks.js +464 -0
  122. package/dist/source/blocks.js.map +1 -0
  123. package/dist/source/code.d.ts +9 -0
  124. package/dist/source/code.d.ts.map +1 -0
  125. package/dist/source/code.js +16 -0
  126. package/dist/source/code.js.map +1 -0
  127. package/dist/source/inline.d.ts +42 -0
  128. package/dist/source/inline.d.ts.map +1 -0
  129. package/dist/source/inline.js +396 -0
  130. package/dist/source/inline.js.map +1 -0
  131. package/dist/source/jsx.d.ts +18 -0
  132. package/dist/source/jsx.d.ts.map +1 -0
  133. package/dist/source/jsx.js +137 -0
  134. package/dist/source/jsx.js.map +1 -0
  135. package/dist/source/lines.d.ts +34 -0
  136. package/dist/source/lines.d.ts.map +1 -0
  137. package/dist/source/lines.js +83 -0
  138. package/dist/source/lines.js.map +1 -0
  139. package/dist/source/markdown.d.ts +23 -0
  140. package/dist/source/markdown.d.ts.map +1 -0
  141. package/dist/source/markdown.js +42 -0
  142. package/dist/source/markdown.js.map +1 -0
  143. package/dist/source/suppressions.d.ts +38 -0
  144. package/dist/source/suppressions.d.ts.map +1 -0
  145. package/dist/source/suppressions.js +69 -0
  146. package/dist/source/suppressions.js.map +1 -0
  147. package/dist/source/tsdoc.d.ts +40 -0
  148. package/dist/source/tsdoc.d.ts.map +1 -0
  149. package/dist/source/tsdoc.js +176 -0
  150. package/dist/source/tsdoc.js.map +1 -0
  151. package/dist/text/grammar.d.ts +91 -0
  152. package/dist/text/grammar.d.ts.map +1 -0
  153. package/dist/text/grammar.js +289 -0
  154. package/dist/text/grammar.js.map +1 -0
  155. package/dist/text/phrases.d.ts +30 -0
  156. package/dist/text/phrases.d.ts.map +1 -0
  157. package/dist/text/phrases.js +65 -0
  158. package/dist/text/phrases.js.map +1 -0
  159. package/dist/text/sentences.d.ts +45 -0
  160. package/dist/text/sentences.d.ts.map +1 -0
  161. package/dist/text/sentences.js +141 -0
  162. package/dist/text/sentences.js.map +1 -0
  163. package/dist/text/tokens.d.ts +35 -0
  164. package/dist/text/tokens.d.ts.map +1 -0
  165. package/dist/text/tokens.js +113 -0
  166. package/dist/text/tokens.js.map +1 -0
  167. package/dist/text/words.d.ts +58 -0
  168. package/dist/text/words.d.ts.map +1 -0
  169. package/dist/text/words.js +159 -0
  170. package/dist/text/words.js.map +1 -0
  171. package/dist/types.d.ts +18 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +20 -0
  174. package/dist/types.js.map +1 -0
  175. package/dist/units.d.ts +47 -0
  176. package/dist/units.d.ts.map +1 -0
  177. package/dist/units.js +42 -0
  178. package/dist/units.js.map +1 -0
  179. package/dist/word-list.d.ts +34 -0
  180. package/dist/word-list.d.ts.map +1 -0
  181. package/dist/word-list.js +90 -0
  182. package/dist/word-list.js.map +1 -0
  183. package/package.json +54 -3
  184. package/src/cli.ts +9 -0
  185. package/src/config-core.ts +258 -0
  186. package/src/config.test.ts +188 -0
  187. package/src/config.ts +83 -0
  188. package/src/core.test.ts +87 -0
  189. package/src/core.ts +32 -0
  190. package/src/dictionary-core.ts +158 -0
  191. package/src/dictionary.test.ts +82 -0
  192. package/src/dictionary.ts +26 -0
  193. package/src/engine.ts +138 -0
  194. package/src/files.test.ts +107 -0
  195. package/src/files.ts +191 -0
  196. package/src/format.ts +78 -0
  197. package/src/index.test.ts +28 -0
  198. package/src/index.ts +38 -0
  199. package/src/lexicon.ts +92 -0
  200. package/src/lint.test.ts +182 -0
  201. package/src/lint.ts +30 -0
  202. package/src/rules/about-quantity.ts +33 -0
  203. package/src/rules/index.ts +40 -0
  204. package/src/rules/ing-verb.test.ts +48 -0
  205. package/src/rules/ing-verb.ts +54 -0
  206. package/src/rules/latin-abbreviation.ts +46 -0
  207. package/src/rules/missing-subject.test.ts +53 -0
  208. package/src/rules/missing-subject.ts +129 -0
  209. package/src/rules/modal-verb.test.ts +45 -0
  210. package/src/rules/modal-verb.ts +72 -0
  211. package/src/rules/noun-cluster.test.ts +49 -0
  212. package/src/rules/noun-cluster.ts +82 -0
  213. package/src/rules/paragraph-length.test.ts +30 -0
  214. package/src/rules/paragraph-length.ts +19 -0
  215. package/src/rules/passive-instruction.test.ts +38 -0
  216. package/src/rules/passive-instruction.ts +37 -0
  217. package/src/rules/pronouns.test.ts +38 -0
  218. package/src/rules/pronouns.ts +54 -0
  219. package/src/rules/punctuation.test.ts +59 -0
  220. package/src/rules/rule.ts +73 -0
  221. package/src/rules/semicolon.ts +19 -0
  222. package/src/rules/sentence-length.test.ts +52 -0
  223. package/src/rules/sentence-length.ts +32 -0
  224. package/src/rules/unknown-word.test.ts +81 -0
  225. package/src/rules/unknown-word.ts +43 -0
  226. package/src/rules/word-list-matcher.ts +68 -0
  227. package/src/rules/word-list.test.ts +110 -0
  228. package/src/rules/word-list.ts +25 -0
  229. package/src/run-cli.test.ts +189 -0
  230. package/src/run-cli.ts +193 -0
  231. package/src/source/blocks.ts +534 -0
  232. package/src/source/code.ts +17 -0
  233. package/src/source/inline.test.ts +92 -0
  234. package/src/source/inline.ts +416 -0
  235. package/src/source/jsx.test.ts +46 -0
  236. package/src/source/jsx.ts +147 -0
  237. package/src/source/lines.test.ts +51 -0
  238. package/src/source/lines.ts +91 -0
  239. package/src/source/markdown.test.ts +210 -0
  240. package/src/source/markdown.ts +49 -0
  241. package/src/source/suppressions.ts +79 -0
  242. package/src/source/tsdoc.test.ts +137 -0
  243. package/src/source/tsdoc.ts +189 -0
  244. package/src/text/grammar.ts +304 -0
  245. package/src/text/phrases.ts +72 -0
  246. package/src/text/sentences.ts +148 -0
  247. package/src/text/text.test.ts +262 -0
  248. package/src/text/tokens.ts +135 -0
  249. package/src/text/words.ts +183 -0
  250. package/src/types.ts +39 -0
  251. package/src/units.ts +86 -0
  252. package/src/word-list.ts +128 -0
  253. package/ste.config.schema.json +87 -0
@@ -0,0 +1,107 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, relative, sep } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { findFiles, globBase, globToRegExp, isGlob } from "./files.js";
6
+
7
+ describe("globToRegExp", () => {
8
+ const matches = (glob : string, path : string) : boolean => globToRegExp(glob).test(path);
9
+
10
+ it("matches *, ** and ?", () => {
11
+ expect(matches("*.md", "README.md")).toBe(true);
12
+ expect(matches("*.md", "docs/a.md")).toBe(false);
13
+ expect(matches("docs/**/*.md", "docs/a.md")).toBe(true);
14
+ expect(matches("docs/**/*.md", "docs/x/y/a.md")).toBe(true);
15
+ expect(matches("**/node_modules/**", "node_modules/a/b.js")).toBe(true);
16
+ expect(matches("**/node_modules/**", "packages/x/node_modules/a.js")).toBe(true);
17
+ expect(matches("**/dist/**", "packages/x/dist/")).toBe(true);
18
+ expect(matches("a?.ts", "ab.ts")).toBe(true);
19
+ expect(matches("a?.ts", "a/.ts")).toBe(false);
20
+ expect(matches("src/**", "src/a/b.ts")).toBe(true);
21
+ expect(matches("a**b", "axxb")).toBe(true);
22
+ });
23
+
24
+ it("matches alternatives and character sets", () => {
25
+ expect(matches("packages/*/src/**/*.{ts,tsx}", "packages/lag/src/a/b.tsx")).toBe(true);
26
+ expect(matches("packages/*/src/**/*.{ts,tsx}", "packages/lag/src/a.js")).toBe(false);
27
+ expect(matches("*.{md,{mdx,markdown}}", "a.markdown")).toBe(true);
28
+ expect(matches("file[0-9].md", "file3.md")).toBe(true);
29
+ expect(matches("file[!0-9].md", "file3.md")).toBe(false);
30
+ expect(matches("a{b", "a{b")).toBe(true);
31
+ expect(matches("a[b", "a[b")).toBe(true);
32
+ });
33
+
34
+ it("escapes regular expression characters and accepts ./ and backslashes", () => {
35
+ expect(matches("a+b.(c)", "a+b.(c)")).toBe(true);
36
+ expect(matches("./docs\\*.md", "docs/a.md")).toBe(true);
37
+ });
38
+
39
+ it("finds the static base directory of a pattern", () => {
40
+ expect(globBase("packages/*/src/**/*.ts")).toBe("packages");
41
+ expect(globBase("docs/**/*.md")).toBe("docs");
42
+ expect(globBase("*.md")).toBe("");
43
+ expect(isGlob("README.md")).toBe(false);
44
+ expect(isGlob("*.md")).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe("findFiles", () => {
49
+ let root : string;
50
+
51
+ beforeEach(() => {
52
+ root = mkdtempSync(join(tmpdir(), "ste-files-"));
53
+ const files = [
54
+ "README.md",
55
+ "docs/guide.md",
56
+ "docs/deep/page.mdx",
57
+ "packages/a/src/index.ts",
58
+ "packages/a/src/index.test.ts",
59
+ "packages/a/dist/index.js",
60
+ "packages/a/node_modules/x/index.ts",
61
+ "packages/b/src/view.tsx",
62
+ ];
63
+ for (const file of files) {
64
+ mkdirSync(join(root, file, ".."), { recursive : true });
65
+ writeFileSync(join(root, file), "x");
66
+ }
67
+ });
68
+
69
+ afterEach(() => {
70
+ rmSync(root, { recursive : true, force : true });
71
+ });
72
+
73
+ const find = (include : string[], ignore : string[] = ["**/*.test.ts", "**/dist/**"]) : string[] =>
74
+ findFiles({ root, include, ignore }).map((file) => relative(root, file).split(sep).join("/"));
75
+
76
+ it("finds the files that match, without ignored files, node_modules or duplicates, in order", () => {
77
+ expect(find(["README.md", "docs/**/*.{md,mdx}", "packages/*/src/**/*.{ts,tsx}", "docs/guide.md"])).toEqual([
78
+ "README.md",
79
+ "docs/deep/page.mdx",
80
+ "docs/guide.md",
81
+ "packages/a/src/index.ts",
82
+ "packages/b/src/view.tsx",
83
+ ]);
84
+ });
85
+
86
+ it("lints a named file even when an ignore pattern matches it, and skips a missing file or directory", () => {
87
+ expect(find(["packages/a/src/index.test.ts", "missing.md", "docs", "nothing/**/*.md"])).toEqual(["packages/a/src/index.test.ts"]);
88
+ });
89
+
90
+ it("accepts absolute glob patterns, also for the ignore patterns", () => {
91
+ const absolute = root.split(sep).join("/");
92
+ expect(find([`${absolute}/docs/**/*.{md,mdx}`])).toEqual(["docs/deep/page.mdx", "docs/guide.md"]);
93
+ expect(find([`${absolute}/packages/*/src/**/*.ts`], [`${absolute}/packages/**/*.test.ts`])).toEqual(["packages/a/src/index.ts"]);
94
+ // A pattern outside the root
95
+ const docs = join(root, "docs");
96
+ expect(findFiles({ root : docs, include : [`${absolute}/packages/b/**/*.tsx`], ignore : [] }).map((file) => relative(root, file).split(sep).join("/")))
97
+ .toEqual(["packages/b/src/view.tsx"]);
98
+ });
99
+
100
+ it("applies the ignore patterns relative to ignoreRoot", () => {
101
+ const packages = join(root, "packages");
102
+ const found = findFiles({ root : packages, include : ["**/*.ts"], ignore : ["packages/a/**"], ignoreRoot : root });
103
+ expect(found.map((file) => relative(root, file).split(sep).join("/"))).toEqual([]);
104
+ const all = findFiles({ root : packages, include : ["**/*.{ts,tsx}"], ignore : ["**/node_modules/**", "packages/a/**"], ignoreRoot : root });
105
+ expect(all.map((file) => relative(root, file).split(sep).join("/"))).toEqual(["packages/b/src/view.tsx"]);
106
+ });
107
+ });
package/src/files.ts ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * File discovery: glob patterns to files.
3
+ *
4
+ * The glob syntax:
5
+ *
6
+ * - `*` matches characters in one path segment
7
+ * - `**` matches zero or more segments
8
+ * - `?` matches one character
9
+ * - `{a,b}` matches one of the alternatives
10
+ * - `[abc]` or `[!abc]` matches one character of a set.
11
+ *
12
+ * Patterns use `/` on all platforms. An absolute pattern is also correct.
13
+ */
14
+
15
+ import { existsSync, readdirSync, statSync } from "node:fs";
16
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
17
+
18
+ const GLOB_CHARS = /[*?[\]{}]/;
19
+
20
+ /** True when `pattern` has glob characters. */
21
+ export function isGlob(pattern : string) : boolean {
22
+ return GLOB_CHARS.test(pattern);
23
+ }
24
+
25
+ function escape(char : string) : string {
26
+ return /[.+^$()|\\[\]{}]/.test(char) ? `\\${char}` : char;
27
+ }
28
+
29
+ /** The index of the `}` that closes the `{` at `open`, or -1. */
30
+ function closingBrace(glob : string, open : number) : number {
31
+ let depth = 0;
32
+ for (let i = open; i < glob.length; i++) {
33
+ if (glob[i] === "{") depth++;
34
+ if (glob[i] === "}" && --depth === 0) return i;
35
+ }
36
+ return -1;
37
+ }
38
+
39
+ /** The alternatives of a `{...}` group. A comma in a nested group does not divide alternatives. */
40
+ function splitAlternatives(body : string) : string[] {
41
+ const parts : string[] = [];
42
+ let depth = 0;
43
+ let start = 0;
44
+ for (let i = 0; i < body.length; i++) {
45
+ if (body[i] === "{") depth++;
46
+ if (body[i] === "}") depth--;
47
+ if (body[i] === "," && depth === 0) {
48
+ parts.push(body.slice(start, i));
49
+ start = i + 1;
50
+ }
51
+ }
52
+ parts.push(body.slice(start));
53
+ return parts;
54
+ }
55
+
56
+ function globSource(glob : string) : string {
57
+ let source = "";
58
+ let i = 0;
59
+ while (i < glob.length) {
60
+ const char = glob[i]!;
61
+ if (char === "*") {
62
+ if (glob[i + 1] === "*") {
63
+ const atStart = i === 0 || glob[i - 1] === "/";
64
+ const atEnd = i + 2 === glob.length || glob[i + 2] === "/";
65
+ if (atStart && atEnd) {
66
+ if (glob[i + 2] === "/") {
67
+ source += "(?:.*/)?";
68
+ i += 3;
69
+ } else {
70
+ source += ".*";
71
+ i += 2;
72
+ }
73
+ continue;
74
+ }
75
+ }
76
+ source += "[^/]*";
77
+ i++;
78
+ while (glob[i] === "*") i++;
79
+ continue;
80
+ }
81
+ if (char === "?") {
82
+ source += "[^/]";
83
+ i++;
84
+ continue;
85
+ }
86
+ if (char === "[") {
87
+ const close = glob.indexOf("]", i + 1);
88
+ if (close !== -1) {
89
+ let set = glob.slice(i + 1, close);
90
+ if (set.startsWith("!")) set = "^" + set.slice(1);
91
+ source += `[${set.replace(/\\/g, "\\\\")}]`;
92
+ i = close + 1;
93
+ continue;
94
+ }
95
+ }
96
+ if (char === "{") {
97
+ const close = closingBrace(glob, i);
98
+ if (close !== -1) {
99
+ source += `(?:${splitAlternatives(glob.slice(i + 1, close)).map(globSource).join("|")})`;
100
+ i = close + 1;
101
+ continue;
102
+ }
103
+ }
104
+ source += escape(char);
105
+ i++;
106
+ }
107
+ return source;
108
+ }
109
+
110
+ /** This function converts a glob pattern to a regular expression that matches a whole relative path. */
111
+ export function globToRegExp(glob : string) : RegExp {
112
+ const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "");
113
+ return new RegExp(`^${globSource(normalized)}$`);
114
+ }
115
+
116
+ /** The part of a pattern before the first segment with glob characters. The walk starts in that directory. */
117
+ export function globBase(glob : string) : string {
118
+ const segments = glob.replace(/\\/g, "/").split("/");
119
+ const base : string[] = [];
120
+ for (const segment of segments.slice(0, -1)) {
121
+ if (isGlob(segment)) break;
122
+ base.push(segment);
123
+ }
124
+ return base.join("/");
125
+ }
126
+
127
+ function toPosix(path : string) : string {
128
+ return path.split(sep).join("/");
129
+ }
130
+
131
+ /** The pattern with `/`, relative to `root`. An absolute pattern becomes relative to `root`, so that it can match a relative path. */
132
+ function relativePattern(root : string, pattern : string) : string {
133
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
134
+ return isAbsolute(normalized) ? toPosix(relative(root, normalized)) : normalized;
135
+ }
136
+
137
+ /** Directories that the walk does not enter. */
138
+ const SKIPPED_DIRECTORIES = new Set(["node_modules", ".git"]);
139
+
140
+ export type FindOptions = {
141
+ /** The directory that the include patterns are relative to. */
142
+ readonly root : string;
143
+ readonly include : readonly string[];
144
+ readonly ignore : readonly string[];
145
+ /** The directory that the ignore patterns are relative to. The default is `root`. */
146
+ readonly ignoreRoot? : string;
147
+ };
148
+
149
+ /**
150
+ * This function finds the files that match the include patterns and no
151
+ * ignore pattern. A pattern without glob characters is a file name, and
152
+ * the ignore patterns do not apply to it. The result has absolute paths,
153
+ * in sorted order, without duplicates.
154
+ */
155
+ export function findFiles(options : FindOptions) : string[] {
156
+ const root = resolve(options.root);
157
+ const ignoreRoot = resolve(options.ignoreRoot ?? root);
158
+ const ignore = options.ignore.map((pattern) => globToRegExp(relativePattern(ignoreRoot, pattern)));
159
+ const isIgnored = (path : string, suffix = "") : boolean => {
160
+ const relativePath = toPosix(relative(ignoreRoot, path)) + suffix;
161
+ return ignore.some((pattern) => pattern.test(relativePath));
162
+ };
163
+ const found = new Set<string>();
164
+
165
+ for (const pattern of options.include) {
166
+ const normalized = relativePattern(root, pattern);
167
+ if (!isGlob(normalized)) {
168
+ const path = resolve(root, normalized);
169
+ if (existsSync(path) && statSync(path).isFile()) found.add(path);
170
+ continue;
171
+ }
172
+ const matcher = globToRegExp(normalized);
173
+ const base = resolve(root, globBase(normalized));
174
+ if (!existsSync(base)) continue;
175
+ const walk = (directory : string) : void => {
176
+ for (const entry of readdirSync(directory, { withFileTypes : true })) {
177
+ const path = join(directory, entry.name);
178
+ const relativePath = toPosix(relative(root, path));
179
+ if (entry.isDirectory()) {
180
+ // A pattern such as "**/dist/**" matches the files in a directory, so test the directory with a "/" after it.
181
+ if (SKIPPED_DIRECTORIES.has(entry.name) || isIgnored(path) || isIgnored(path, "/")) continue;
182
+ walk(path);
183
+ } else if (entry.isFile() && matcher.test(relativePath) && !isIgnored(path)) {
184
+ found.add(path);
185
+ }
186
+ }
187
+ };
188
+ walk(base);
189
+ }
190
+ return [...found].sort();
191
+ }
package/src/format.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { RULE_IDS, type Finding, type RuleId } from "./types.js";
2
+
3
+ export type RuleCount = {
4
+ readonly errors : number;
5
+ readonly warnings : number;
6
+ };
7
+
8
+ export type Summary = {
9
+ /** The number of files that the linter examined. */
10
+ readonly files : number;
11
+ readonly filesWithFindings : number;
12
+ readonly errors : number;
13
+ readonly warnings : number;
14
+ /** The counts of each rule that has findings, in the order of the rule list. */
15
+ readonly byRule : Readonly<Partial<Record<RuleId, RuleCount>>>;
16
+ };
17
+
18
+ /** This function counts the findings, by severity and by rule. */
19
+ export function summarize(findings : readonly Finding[], files : number) : Summary {
20
+ const byRule : Partial<Record<RuleId, { errors : number; warnings : number }>> = {};
21
+ for (const id of RULE_IDS) {
22
+ const own = findings.filter((finding) => finding.ruleId === id);
23
+ if (own.length === 0) continue;
24
+ byRule[id] = {
25
+ errors : own.filter((finding) => finding.severity === "error").length,
26
+ warnings : own.filter((finding) => finding.severity === "warning").length,
27
+ };
28
+ }
29
+ return {
30
+ files,
31
+ filesWithFindings : new Set(findings.map((finding) => finding.file)).size,
32
+ errors : findings.filter((finding) => finding.severity === "error").length,
33
+ warnings : findings.filter((finding) => finding.severity === "warning").length,
34
+ byRule,
35
+ };
36
+ }
37
+
38
+ const plural = (count : number, word : string) : string => `${count} ${word}${count === 1 ? "" : "s"}`;
39
+
40
+ /** The findings as text: one group for each file, then a summary and the counts by rule. */
41
+ export function formatText(findings : readonly Finding[], summary : Summary) : string {
42
+ const lines : string[] = [];
43
+ const byFile = new Map<string, Finding[]>();
44
+ for (const finding of findings) {
45
+ const list = byFile.get(finding.file) ?? [];
46
+ list.push(finding);
47
+ byFile.set(finding.file, list);
48
+ }
49
+ for (const [file, list] of byFile) {
50
+ lines.push(file);
51
+ const positions = list.map((finding) => `${finding.line}:${finding.column}`);
52
+ const width = Math.max(...positions.map((position) => position.length));
53
+ list.forEach((finding, index) => {
54
+ lines.push(` ${positions[index]!.padEnd(width)} ${finding.severity.padEnd(7)} ${finding.message} ${finding.ruleId}`);
55
+ });
56
+ lines.push("");
57
+ }
58
+ const total = summary.errors + summary.warnings;
59
+ if (total === 0) {
60
+ lines.push(`No problems found in ${plural(summary.files, "file")}.`);
61
+ return lines.join("\n") + "\n";
62
+ }
63
+ lines.push(`${plural(total, "problem")} (${plural(summary.errors, "error")}, ${plural(summary.warnings, "warning")}) in ${plural(summary.filesWithFindings, "file")} of ${summary.files}.`);
64
+ lines.push("");
65
+ lines.push("Findings by rule:");
66
+ const entries = Object.entries(summary.byRule) as Array<[RuleId, RuleCount]>;
67
+ const width = Math.max(...entries.map(([id]) => id.length));
68
+ for (const [id, count] of entries) {
69
+ const parts = [count.errors > 0 ? plural(count.errors, "error") : null, count.warnings > 0 ? plural(count.warnings, "warning") : null].filter((part) => part !== null);
70
+ lines.push(` ${id.padEnd(width)} ${String(count.errors + count.warnings).padStart(5)} (${parts.join(", ")})`);
71
+ }
72
+ return lines.join("\n") + "\n";
73
+ }
74
+
75
+ /** The findings and the summary as JSON. */
76
+ export function formatJson(findings : readonly Finding[], summary : Summary) : string {
77
+ return JSON.stringify({ tool : "ste-lint", summary, findings }, null, 2) + "\n";
78
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as api from "./index.js";
3
+
4
+ describe("public API", () => {
5
+ it("exports the linter, the configuration, the dictionary, the CLI and the rules", () => {
6
+ for (const name of [
7
+ "Linter", "lintText", "fileKind", "loadConfig", "resolveConfig", "validateConfig", "findConfigFile", "mergeWordList",
8
+ "ConfigError", "Dictionary", "DictionaryError", "loadDictionary", "parseDictionary", "findFiles", "globToRegExp",
9
+ "formatJson", "formatText", "summarize", "buildLexicon", "getRule", "RULES", "parseArgs", "runCli", "isRuleId",
10
+ ]) {
11
+ expect(api).toHaveProperty(name);
12
+ }
13
+ expect(api.CONFIG_FILE_NAME).toBe("ste.config.json");
14
+ expect(api.DICTIONARY_ENV).toBe("STE_DICTIONARY");
15
+ expect(api.DICTIONARY_FORMAT).toBe("ste-lint-dictionary");
16
+ expect(api.RULE_IDS).toHaveLength(14);
17
+ expect(api.DEFAULT_LIMITS.instructionWords).toBe(20);
18
+ expect(api.DEFAULT_INCLUDE).toContain("README.md");
19
+ expect(api.DEFAULT_IGNORE).toContain("**/node_modules/**");
20
+ expect(api.DEFAULT_WORD_LIST.length).toBeGreaterThan(40);
21
+ expect(api.USAGE).toContain("ste-lint");
22
+ });
23
+
24
+ it("lints text with the defaults", () => {
25
+ const config = api.resolveConfig({}, { root : "/" });
26
+ expect(api.lintText("You should stop.", "a.md", config).map((finding) => finding.ruleId)).toEqual(["modal-verb"]);
27
+ });
28
+ });
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `@mark1russell7/ste-lint`: a linter for prose in Markdown, MDX and TypeScript doc
3
+ * comments. It is an automated approximation of the writing rules of
4
+ * ASD-STE100 Simplified Technical English. It does not certify that a text
5
+ * obeys the standard.
6
+ */
7
+
8
+ export {
9
+ baseConfig,
10
+ CONFIG_FILE_NAME,
11
+ ConfigError,
12
+ DEFAULT_IGNORE,
13
+ DEFAULT_INCLUDE,
14
+ DEFAULT_LIMITS,
15
+ DICTIONARY_ENV,
16
+ findConfigFile,
17
+ loadConfig,
18
+ mergeWordList,
19
+ resolveConfig,
20
+ validateConfig,
21
+ type BaseConfigOptions,
22
+ type Limits,
23
+ type LimitsConfig,
24
+ type ResolvedConfig,
25
+ type SteConfigFile,
26
+ type WordListEntryConfig,
27
+ } from "./config.js";
28
+ export { Dictionary, DICTIONARY_FORMAT, DictionaryError, loadDictionary, parseDictionary, type DictionaryEntry, type DictionaryLookup } from "./dictionary.js";
29
+ export { findFiles, globToRegExp } from "./files.js";
30
+ export { formatJson, formatText, summarize, type RuleCount, type Summary } from "./format.js";
31
+ export { buildLexicon, type Lexicon } from "./lexicon.js";
32
+ export { ProseLinter, type CodeExtractor, type ProseLinterOptions } from "./engine.js";
33
+ export { fileKind, Linter, lintText, type FileKind } from "./lint.js";
34
+ export { extractCode } from "./source/code.js";
35
+ export { getRule, RULES, type Rule } from "./rules/index.js";
36
+ export { parseArgs, runCli, USAGE, type CliIo, type CliOptions } from "./run-cli.js";
37
+ export { isRuleId, RULE_IDS, type Finding, type RuleId, type RuleSetting, type Severity } from "./types.js";
38
+ export { DEFAULT_WORD_LIST, type WordListEntry } from "./word-list.js";
package/src/lexicon.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The word sets that the rules use: the project glossary from the
3
+ * configuration, the built-in word lists, and the local dictionary when
4
+ * there is one.
5
+ */
6
+
7
+ import type { ResolvedConfig } from "./config.js";
8
+ import type { Dictionary } from "./dictionary.js";
9
+ import { hasLemmaIn } from "./text/grammar.js";
10
+ import { PhraseMatcher, splitPhrase } from "./text/phrases.js";
11
+ import { COMMON_NOUNS, COMMON_VERBS } from "./text/words.js";
12
+
13
+ export type Lexicon = {
14
+ /** Single-word technical nouns, in lower case. */
15
+ readonly technicalNouns : ReadonlySet<string>;
16
+ /** Single-word technical verbs, in lower case. */
17
+ readonly technicalVerbs : ReadonlySet<string>;
18
+ /** Single-word proper nouns, in lower case. */
19
+ readonly properNouns : ReadonlySet<string>;
20
+ readonly allowedWords : ReadonlySet<string>;
21
+ /** All glossary terms, single words and phrases, for the unknown-word rule. */
22
+ readonly glossaryPhrases : PhraseMatcher;
23
+ /** Multi-word technical nouns. The noun-cluster rule counts each one as one noun. */
24
+ readonly technicalNounPhrases : PhraseMatcher;
25
+ /** Multi-word proper nouns. Each one counts as one word (STE Rule 8.6). */
26
+ readonly properNounPhrases : PhraseMatcher;
27
+ /** Nouns for the noun-cluster rule. */
28
+ readonly nouns : ReadonlySet<string>;
29
+ /** Base forms of verbs for the imperative and missing-subject heuristics. */
30
+ readonly verbs : ReadonlySet<string>;
31
+ readonly dictionary : Dictionary | null;
32
+ };
33
+
34
+ function words(terms : readonly string[]) : { single : Set<string>; phrases : string[] } {
35
+ const single = new Set<string>();
36
+ const phrases : string[] = [];
37
+ for (const term of terms) {
38
+ const parts = splitPhrase(term);
39
+ if (parts.length === 1) single.add(parts[0]!);
40
+ else if (parts.length > 1) phrases.push(parts.join(" "));
41
+ }
42
+ return { single, phrases };
43
+ }
44
+
45
+ /** This function makes the lexicon for a configuration and an optional dictionary. */
46
+ export function buildLexicon(config : ResolvedConfig, dictionary : Dictionary | null = null) : Lexicon {
47
+ const nouns = words(config.technicalNouns);
48
+ const verbs = words(config.technicalVerbs);
49
+ const proper = words(config.properNouns);
50
+ const allowed = words(config.allowedWords);
51
+
52
+ const glossaryPhrases = new PhraseMatcher();
53
+ const technicalNounPhrases = new PhraseMatcher();
54
+ // A name counts as one word only with its own capital letters: "Event Timing", but not "event timing".
55
+ const properNounPhrases = new PhraseMatcher({ caseSensitive : true });
56
+ for (const phrase of nouns.phrases) {
57
+ glossaryPhrases.add(phrase, true);
58
+ technicalNounPhrases.add(phrase, true);
59
+ }
60
+ for (const phrase of verbs.phrases) glossaryPhrases.add(phrase, true);
61
+ for (const phrase of proper.phrases) glossaryPhrases.add(phrase, true);
62
+ for (const name of config.properNouns) {
63
+ if (name.trim().split(/\s+/).length > 1) properNounPhrases.add(name.trim(), true);
64
+ }
65
+
66
+ const verbSet = new Set<string>([...COMMON_VERBS, ...verbs.single, ...(dictionary?.verbs ?? [])]);
67
+ // A word that is often a verb counts as a noun only when the glossary or the dictionary says that it is a noun.
68
+ const nounSet = new Set<string>();
69
+ for (const noun of COMMON_NOUNS) if (!verbSet.has(noun)) nounSet.add(noun);
70
+ for (const noun of [...nouns.single, ...proper.single, ...(dictionary?.nouns ?? [])]) nounSet.add(noun);
71
+
72
+ return {
73
+ technicalNouns : nouns.single,
74
+ technicalVerbs : verbs.single,
75
+ properNouns : proper.single,
76
+ allowedWords : allowed.single,
77
+ glossaryPhrases,
78
+ technicalNounPhrases,
79
+ properNounPhrases,
80
+ nouns : nounSet,
81
+ verbs : verbSet,
82
+ dictionary,
83
+ };
84
+ }
85
+
86
+ /** True when the glossary permits the word: a technical noun or verb in any form, a proper noun, or a word of `allowedWords`. */
87
+ export function isGlossaryWord(word : string, lexicon : Lexicon) : boolean {
88
+ return lexicon.properNouns.has(word)
89
+ || lexicon.allowedWords.has(word)
90
+ || hasLemmaIn(word, lexicon.technicalNouns)
91
+ || hasLemmaIn(word, lexicon.technicalVerbs);
92
+ }