nollm 0.0.0
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/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/nollm.js +4 -0
- package/package.json +58 -0
- package/src/check.js +86 -0
- package/src/cli.js +114 -0
- package/src/comments.js +156 -0
- package/src/config.js +110 -0
- package/src/files.js +118 -0
- package/src/index.d.ts +88 -0
- package/src/index.js +7 -0
- package/src/languages.js +266 -0
- package/src/lint.js +51 -0
- package/src/rules.js +321 -0
- package/src/worker.js +39 -0
package/src/rules.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule is a regular expression plus a message.
|
|
3
|
+
*
|
|
4
|
+
* scope controls where the rule runs:
|
|
5
|
+
* prose → markdown and text files
|
|
6
|
+
* comments → comments inside code files
|
|
7
|
+
* everywhere → every line of every file
|
|
8
|
+
*
|
|
9
|
+
* A rule with no scope runs in prose and in comments.
|
|
10
|
+
*
|
|
11
|
+
* Patterns must have the g flag.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function words(list) {
|
|
15
|
+
return list.map(escape).join("|");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function anyOf(list) {
|
|
19
|
+
return new RegExp(String.raw`\b(?:${words(list)})\b`, "gi");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escape(text) {
|
|
23
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BANNED_WORDS = [
|
|
27
|
+
"genuinely",
|
|
28
|
+
"fails loudly",
|
|
29
|
+
"fails quietly",
|
|
30
|
+
"spearheaded",
|
|
31
|
+
"exactly the crutch",
|
|
32
|
+
"crutch",
|
|
33
|
+
"load bearing",
|
|
34
|
+
"load-bearing",
|
|
35
|
+
"carrying",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const FILLER_WORDS = [
|
|
39
|
+
"simply",
|
|
40
|
+
"seamlessly",
|
|
41
|
+
"seamless",
|
|
42
|
+
"robust",
|
|
43
|
+
"powerful",
|
|
44
|
+
"comprehensive",
|
|
45
|
+
"leverage",
|
|
46
|
+
"leverages",
|
|
47
|
+
"leveraged",
|
|
48
|
+
"leveraging",
|
|
49
|
+
"utilize",
|
|
50
|
+
"utilizes",
|
|
51
|
+
"utilized",
|
|
52
|
+
"utilizing",
|
|
53
|
+
"in order to",
|
|
54
|
+
"prior to",
|
|
55
|
+
"in the event that",
|
|
56
|
+
"it is worth noting",
|
|
57
|
+
"it's worth noting",
|
|
58
|
+
"it is important to note",
|
|
59
|
+
"it's important to note",
|
|
60
|
+
"it should be noted",
|
|
61
|
+
"keep in mind",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const LLM_VOCABULARY = [
|
|
65
|
+
"delve",
|
|
66
|
+
"delves",
|
|
67
|
+
"delving",
|
|
68
|
+
"tapestry",
|
|
69
|
+
"streamline",
|
|
70
|
+
"streamlines",
|
|
71
|
+
"streamlined",
|
|
72
|
+
"cutting-edge",
|
|
73
|
+
"game-changer",
|
|
74
|
+
"game changer",
|
|
75
|
+
"at its core",
|
|
76
|
+
"deep dive",
|
|
77
|
+
"dive into",
|
|
78
|
+
"let's dive",
|
|
79
|
+
"multifaceted",
|
|
80
|
+
"pivotal",
|
|
81
|
+
"crucial",
|
|
82
|
+
"myriad",
|
|
83
|
+
"plethora",
|
|
84
|
+
"foster",
|
|
85
|
+
"fosters",
|
|
86
|
+
"meticulous",
|
|
87
|
+
"meticulously",
|
|
88
|
+
"a testament to",
|
|
89
|
+
"in today's",
|
|
90
|
+
"fast-paced",
|
|
91
|
+
"ever-evolving",
|
|
92
|
+
"effortlessly",
|
|
93
|
+
"supercharge",
|
|
94
|
+
"state-of-the-art",
|
|
95
|
+
"battle-tested",
|
|
96
|
+
"production-ready",
|
|
97
|
+
"blazing fast",
|
|
98
|
+
"blazingly fast",
|
|
99
|
+
"lightning-fast",
|
|
100
|
+
"lightning fast",
|
|
101
|
+
"elevate your",
|
|
102
|
+
"empower",
|
|
103
|
+
"empowers",
|
|
104
|
+
"empowering",
|
|
105
|
+
"navigate the complexities",
|
|
106
|
+
"unlock the",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const CHAT_OPENERS = [
|
|
110
|
+
"great question",
|
|
111
|
+
"good question",
|
|
112
|
+
"certainly",
|
|
113
|
+
"absolutely",
|
|
114
|
+
"sure thing",
|
|
115
|
+
"sure",
|
|
116
|
+
"of course",
|
|
117
|
+
"let me",
|
|
118
|
+
"i will",
|
|
119
|
+
"i'll",
|
|
120
|
+
"looking at your",
|
|
121
|
+
"here's a",
|
|
122
|
+
"here is a",
|
|
123
|
+
"i'd be happy to",
|
|
124
|
+
"i would be happy to",
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const CHAT_CLOSERS = [
|
|
128
|
+
"let me know if",
|
|
129
|
+
"hope this helps",
|
|
130
|
+
"hope that helps",
|
|
131
|
+
"feel free to",
|
|
132
|
+
"happy to help",
|
|
133
|
+
"don't hesitate to",
|
|
134
|
+
"do not hesitate to",
|
|
135
|
+
"if you have any questions",
|
|
136
|
+
"if there is anything else",
|
|
137
|
+
"if there's anything else",
|
|
138
|
+
"anything else i can",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const AI_DISCLOSURES = [
|
|
142
|
+
"as an ai",
|
|
143
|
+
"as a language model",
|
|
144
|
+
"as a large language model",
|
|
145
|
+
"i apologize for the confusion",
|
|
146
|
+
"i apologize for any confusion",
|
|
147
|
+
"i don't have access to",
|
|
148
|
+
"i cannot browse",
|
|
149
|
+
"my knowledge cutoff",
|
|
150
|
+
"my training data",
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const ERROR_EXCLAMATIONS = [
|
|
154
|
+
"uh oh",
|
|
155
|
+
"uh-oh",
|
|
156
|
+
"oh no",
|
|
157
|
+
"oops",
|
|
158
|
+
"whoops",
|
|
159
|
+
"there seems to be a problem",
|
|
160
|
+
"something went wrong",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const DIFF_TALK = [
|
|
164
|
+
"as requested",
|
|
165
|
+
"as discussed",
|
|
166
|
+
"per the discussion",
|
|
167
|
+
"per our discussion",
|
|
168
|
+
"per the review",
|
|
169
|
+
"per review",
|
|
170
|
+
"this pr",
|
|
171
|
+
"this commit",
|
|
172
|
+
"this diff",
|
|
173
|
+
"this change",
|
|
174
|
+
"no longer",
|
|
175
|
+
"used to be",
|
|
176
|
+
"used to use",
|
|
177
|
+
"used to return",
|
|
178
|
+
"previously",
|
|
179
|
+
"formerly",
|
|
180
|
+
"the old implementation",
|
|
181
|
+
"the old version",
|
|
182
|
+
"the old code",
|
|
183
|
+
"the old approach",
|
|
184
|
+
"the old way",
|
|
185
|
+
"updated to",
|
|
186
|
+
"changed to",
|
|
187
|
+
"instead of the old",
|
|
188
|
+
"instead of the previous",
|
|
189
|
+
"we removed",
|
|
190
|
+
"we replaced",
|
|
191
|
+
"we migrated",
|
|
192
|
+
"we switched",
|
|
193
|
+
"now uses",
|
|
194
|
+
"now returns",
|
|
195
|
+
"now handles",
|
|
196
|
+
"now lives",
|
|
197
|
+
"moved to",
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const WHAT_COMMENT_STARTS = [
|
|
201
|
+
"this function",
|
|
202
|
+
"this method",
|
|
203
|
+
"this class",
|
|
204
|
+
"this component",
|
|
205
|
+
"this helper",
|
|
206
|
+
"this hook",
|
|
207
|
+
"this file",
|
|
208
|
+
"this module",
|
|
209
|
+
"this variable",
|
|
210
|
+
"this constant",
|
|
211
|
+
"this line",
|
|
212
|
+
"loop over",
|
|
213
|
+
"loop through",
|
|
214
|
+
"iterate over",
|
|
215
|
+
"iterate through",
|
|
216
|
+
"check if",
|
|
217
|
+
"checks if",
|
|
218
|
+
"increment",
|
|
219
|
+
"decrement",
|
|
220
|
+
"set the",
|
|
221
|
+
"sets the",
|
|
222
|
+
"get the",
|
|
223
|
+
"gets the",
|
|
224
|
+
"create a new",
|
|
225
|
+
"creates a new",
|
|
226
|
+
"initialize the",
|
|
227
|
+
"initializes the",
|
|
228
|
+
"import the",
|
|
229
|
+
"imports the",
|
|
230
|
+
"define the",
|
|
231
|
+
"defines the",
|
|
232
|
+
"call the",
|
|
233
|
+
"calls the",
|
|
234
|
+
"declare",
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
// A comment starts after its marker, so the rule skips the marker first.
|
|
238
|
+
const COMMENT_START = String.raw`^\s*(?:\/\/+|#+|\*+|\/\*+|<!--|--|;+|%+|"""|''')?\s*`;
|
|
239
|
+
|
|
240
|
+
export const rules = [
|
|
241
|
+
{
|
|
242
|
+
id: "banned-word",
|
|
243
|
+
message: "Banned word",
|
|
244
|
+
pattern: anyOf(BANNED_WORDS),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "em-dash",
|
|
248
|
+
message: "Em dash. Use a comma, a colon, or a new sentence",
|
|
249
|
+
pattern: /\u2014/g,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "bold-fragment",
|
|
253
|
+
message: "Bold fragment followed by plain text",
|
|
254
|
+
pattern: /^\s*(?:[-*+]|\d+[.)])?\s*(?:\*\*[^*\n]{1,80}\*\*|__[^_\n]{1,80}__):?\s+\S/gm,
|
|
255
|
+
scope: "prose",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "filler-word",
|
|
259
|
+
message: "Filler. Delete it or replace it",
|
|
260
|
+
pattern: anyOf(FILLER_WORDS),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "llm-vocabulary",
|
|
264
|
+
message: "LLM vocabulary",
|
|
265
|
+
pattern: anyOf(LLM_VOCABULARY),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "chat-opener",
|
|
269
|
+
message: "Chat opener. Start with the answer",
|
|
270
|
+
pattern: new RegExp(
|
|
271
|
+
String.raw`${COMMENT_START}(?:[-*+]\s+)?(?:${words(CHAT_OPENERS)})\b[!,:]?`,
|
|
272
|
+
"gmi",
|
|
273
|
+
),
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "chat-closer",
|
|
277
|
+
message: "Chat closer. Stop when the content stops",
|
|
278
|
+
pattern: anyOf(CHAT_CLOSERS),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "ai-disclosure",
|
|
282
|
+
message: "Text written from the point of view of a chat assistant",
|
|
283
|
+
pattern: anyOf(AI_DISCLOSURES),
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "error-exclamation",
|
|
287
|
+
message: "Error message with an exclamation instead of a cause",
|
|
288
|
+
pattern: anyOf(ERROR_EXCLAMATIONS),
|
|
289
|
+
scope: "everywhere",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "contrast-cliche",
|
|
293
|
+
message: "Contrast cliche (not just X, but Y)",
|
|
294
|
+
pattern:
|
|
295
|
+
/\b(?:not (?:just|only|merely|simply) [^.\n]{1,60}?,? but(?: also)?\b|it'?s not (?:just |about )?[^.\n]{1,40}?[,;] it'?s\b)/gi,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "rhetorical-question",
|
|
299
|
+
message: "Rhetorical question followed by its answer",
|
|
300
|
+
pattern:
|
|
301
|
+
/(?:\bwhy\? because\b|\bthe (?:result|catch|problem|solution|answer|best part|good news|bottom line|difference)\?)/gi,
|
|
302
|
+
scope: "prose",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: "emoji",
|
|
306
|
+
message: "Emoji",
|
|
307
|
+
pattern: /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F)/gu,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "diff-comment",
|
|
311
|
+
message: "Comment describes the change, not the code. Put it in the commit message",
|
|
312
|
+
pattern: anyOf(DIFF_TALK),
|
|
313
|
+
scope: "comments",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "what-comment",
|
|
317
|
+
message: "Comment narrates what the code does. Say why, or delete it",
|
|
318
|
+
pattern: new RegExp(String.raw`${COMMENT_START}(?:${words(WHAT_COMMENT_STARTS)})\b`, "gmi"),
|
|
319
|
+
scope: "comments",
|
|
320
|
+
},
|
|
321
|
+
];
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Tinypool } from "tinypool";
|
|
4
|
+
import { check } from "./check.js";
|
|
5
|
+
import { classify } from "./languages.js";
|
|
6
|
+
import { loadConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
const { configPath, cwd } = Tinypool.workerData;
|
|
9
|
+
const config = await loadConfig(configPath);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks one file.
|
|
13
|
+
*
|
|
14
|
+
* Returns { file, findings, skipped }.
|
|
15
|
+
* skipped names the reason a file was not read, or is null.
|
|
16
|
+
*/
|
|
17
|
+
export default async function checkFile(file) {
|
|
18
|
+
if (!classify(file)) return { file, findings: [], skipped: "unknown type" };
|
|
19
|
+
|
|
20
|
+
let buffer;
|
|
21
|
+
try {
|
|
22
|
+
buffer = await readFile(resolve(cwd, file));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return { file, findings: [], skipped: error.code ?? "unreadable" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (buffer.length > config.maxBytes) return { file, findings: [], skipped: "too large" };
|
|
28
|
+
if (isBinary(buffer)) return { file, findings: [], skipped: "binary" };
|
|
29
|
+
|
|
30
|
+
return { file, findings: check(file, buffer.toString("utf8"), config.rules), skipped: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isBinary(buffer) {
|
|
34
|
+
const end = Math.min(buffer.length, 8000);
|
|
35
|
+
for (let i = 0; i < end; i++) {
|
|
36
|
+
if (buffer[i] === 0) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|