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/src/files.js ADDED
@@ -0,0 +1,118 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import ignore from "ignore";
6
+
7
+ const run = promisify(execFile);
8
+
9
+ const ALWAYS_SKIPPED = new Set([".git", "node_modules"]);
10
+
11
+ /**
12
+ * Lists the files to check.
13
+ *
14
+ * Paths come back relative to cwd, with forward slashes.
15
+ * Files that git ignores are left out.
16
+ *
17
+ * Inside a git work tree the list comes from git itself.
18
+ * Elsewhere, or with git: false, the tool reads .gitignore files while it walks.
19
+ *
20
+ * The ignore option takes patterns in .gitignore syntax.
21
+ */
22
+ export async function collectFiles(
23
+ roots,
24
+ { cwd = process.cwd(), git = true, ignore: patterns = [] } = {},
25
+ ) {
26
+ const files = git ? await fromGit(roots, cwd) : null;
27
+ const list = files ?? (await fromWalk(roots, cwd));
28
+ if (patterns.length === 0) return list;
29
+
30
+ const matcher = ignore().add(patterns);
31
+ return list.filter((file) => !matcher.ignores(file));
32
+ }
33
+
34
+ async function fromGit(roots, cwd) {
35
+ const args = [
36
+ "ls-files",
37
+ "-z",
38
+ "--cached",
39
+ "--others",
40
+ "--exclude-standard",
41
+ "--deduplicate",
42
+ "--",
43
+ ];
44
+ for (let i = 0; i < roots.length; i++) args.push(roots[i]);
45
+
46
+ let stdout;
47
+ try {
48
+ ({ stdout } = await run("git", args, { cwd, maxBuffer: 256 * 1024 * 1024 }));
49
+ } catch {
50
+ return null;
51
+ }
52
+
53
+ const files = [];
54
+ const parts = stdout.split("\0");
55
+ for (let i = 0; i < parts.length; i++) {
56
+ if (parts[i].length > 0) files.push(parts[i]);
57
+ }
58
+ return files;
59
+ }
60
+
61
+ async function fromWalk(roots, cwd) {
62
+ const files = [];
63
+ for (let i = 0; i < roots.length; i++) {
64
+ const absolute = resolve(cwd, roots[i]);
65
+ const info = await stat(absolute);
66
+ if (info.isFile()) {
67
+ files.push(toPosix(relative(cwd, absolute)));
68
+ continue;
69
+ }
70
+ await walk(absolute, cwd, [], files);
71
+ }
72
+ return files;
73
+ }
74
+
75
+ async function walk(dir, cwd, filters, files) {
76
+ const local = await readIgnore(dir);
77
+ const active = local ? filters.concat([local]) : filters;
78
+ const entries = await readdir(dir, { withFileTypes: true });
79
+
80
+ for (let i = 0; i < entries.length; i++) {
81
+ const entry = entries[i];
82
+ if (ALWAYS_SKIPPED.has(entry.name)) continue;
83
+
84
+ const absolute = join(dir, entry.name);
85
+ const isDir = entry.isDirectory();
86
+ if (!isDir && !entry.isFile()) continue;
87
+ if (isIgnored(absolute, isDir, active)) continue;
88
+
89
+ if (isDir) {
90
+ await walk(absolute, cwd, active, files);
91
+ } else {
92
+ files.push(toPosix(relative(cwd, absolute)));
93
+ }
94
+ }
95
+ }
96
+
97
+ async function readIgnore(dir) {
98
+ let content;
99
+ try {
100
+ content = await readFile(join(dir, ".gitignore"), "utf8");
101
+ } catch {
102
+ return null;
103
+ }
104
+ return { base: dir, matcher: ignore().add(content) };
105
+ }
106
+
107
+ function isIgnored(absolute, isDir, filters) {
108
+ for (let i = 0; i < filters.length; i++) {
109
+ const { base, matcher } = filters[i];
110
+ const rel = toPosix(relative(base, absolute)) + (isDir ? "/" : "");
111
+ if (matcher.ignores(rel)) return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function toPosix(path) {
117
+ return sep === "/" ? path : path.split(sep).join("/");
118
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ export type Scope = "prose" | "comments" | "everywhere";
2
+
3
+ export interface Rule {
4
+ id: string;
5
+ message: string;
6
+ pattern: RegExp;
7
+ scope?: Scope;
8
+ }
9
+
10
+ export interface Finding {
11
+ line: number;
12
+ column: number;
13
+ ruleId: string;
14
+ message: string;
15
+ text: string;
16
+ }
17
+
18
+ export interface FileResult {
19
+ file: string;
20
+ findings: Finding[];
21
+ skipped: string | null;
22
+ }
23
+
24
+ export interface Summary {
25
+ files: number;
26
+ checked: number;
27
+ findings: number;
28
+ filesWithFindings: number;
29
+ }
30
+
31
+ export interface Segment {
32
+ text: string;
33
+ line: number;
34
+ column: number;
35
+ }
36
+
37
+ export interface Language {
38
+ comments: [start: string, end: string][];
39
+ strings: string[];
40
+ regions: [start: string, end: string, language: Language][];
41
+ }
42
+
43
+ export interface Config {
44
+ path: string | null;
45
+ rules: Rule[];
46
+ ignore: string[];
47
+ maxBytes: number;
48
+ }
49
+
50
+ export interface RuleConfig {
51
+ pattern: RegExp | string;
52
+ flags?: string;
53
+ message?: string;
54
+ scope?: Scope;
55
+ }
56
+
57
+ export interface UserConfig {
58
+ ignore?: string[];
59
+ words?: string[];
60
+ maxBytes?: number;
61
+ rules?: Record<string, boolean | RuleConfig>;
62
+ }
63
+
64
+ export interface LintOptions {
65
+ roots?: string[];
66
+ cwd?: string;
67
+ configPath?: string;
68
+ git?: boolean;
69
+ jobs?: number;
70
+ onResult?: (result: FileResult) => void;
71
+ }
72
+
73
+ export const rules: Rule[];
74
+ export const SEARCH_PLACES: string[];
75
+
76
+ export function check(filePath: string, source: string, rules?: Rule[]): Finding[];
77
+ export function classify(
78
+ filePath: string,
79
+ ): { kind: "prose" } | { kind: "code"; language: Language } | null;
80
+ export function collectFiles(
81
+ roots: string[],
82
+ options?: { cwd?: string; git?: boolean; ignore?: string[] },
83
+ ): Promise<string[]>;
84
+ export function extractComments(source: string, language: Language): Segment[];
85
+ export function extractLines(source: string): Segment[];
86
+ export function findConfig(cwd: string): Promise<string | null>;
87
+ export function loadConfig(configPath: string | null): Promise<Config>;
88
+ export function lint(options?: LintOptions): Promise<Summary>;
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { check } from "./check.js";
2
+ export { classify } from "./languages.js";
3
+ export { collectFiles } from "./files.js";
4
+ export { extractComments, extractLines } from "./comments.js";
5
+ export { findConfig, loadConfig, SEARCH_PLACES } from "./config.js";
6
+ export { lint } from "./lint.js";
7
+ export { rules } from "./rules.js";
@@ -0,0 +1,266 @@
1
+ /**
2
+ * A language describes how to find comments in a file.
3
+ *
4
+ * comments: pairs of start and end markers.
5
+ * An end of "\n" means the comment runs to the end of the line.
6
+ *
7
+ * strings: delimiters that open and close a string.
8
+ * Comment markers inside a string are ignored.
9
+ *
10
+ * regions: nested languages, for example <script> inside HTML.
11
+ */
12
+
13
+ const clike = {
14
+ comments: [
15
+ ["/*", "*/"],
16
+ ["//", "\n"],
17
+ ],
18
+ strings: ['"', "'", "`"],
19
+ regions: [],
20
+ };
21
+
22
+ const css = {
23
+ comments: [["/*", "*/"]],
24
+ strings: ['"', "'"],
25
+ regions: [],
26
+ };
27
+
28
+ const rust = {
29
+ comments: clike.comments,
30
+ strings: ['"'],
31
+ regions: [],
32
+ };
33
+
34
+ const hash = {
35
+ comments: [["#", "\n"]],
36
+ strings: ['"', "'"],
37
+ regions: [],
38
+ };
39
+
40
+ const python = {
41
+ comments: [
42
+ ['"""', '"""'],
43
+ ["'''", "'''"],
44
+ ["#", "\n"],
45
+ ],
46
+ strings: ['"', "'"],
47
+ regions: [],
48
+ };
49
+
50
+ const config = {
51
+ comments: [["#", "\n"]],
52
+ strings: ['"'],
53
+ regions: [],
54
+ };
55
+
56
+ const ruby = {
57
+ comments: [
58
+ ["=begin", "=end"],
59
+ ["#", "\n"],
60
+ ],
61
+ strings: ['"', "'"],
62
+ regions: [],
63
+ };
64
+
65
+ const dash = {
66
+ comments: [["--", "\n"]],
67
+ strings: ['"', "'"],
68
+ regions: [],
69
+ };
70
+
71
+ const lua = {
72
+ comments: [
73
+ ["--[[", "]]"],
74
+ ["--", "\n"],
75
+ ],
76
+ strings: ['"', "'"],
77
+ regions: [],
78
+ };
79
+
80
+ const lisp = {
81
+ comments: [[";", "\n"]],
82
+ strings: ['"'],
83
+ regions: [],
84
+ };
85
+
86
+ const glimmer = {
87
+ comments: [
88
+ ["{{!--", "--}}"],
89
+ ["{{!", "}}"],
90
+ ["<!--", "-->"],
91
+ ],
92
+ strings: [],
93
+ regions: [],
94
+ };
95
+
96
+ const html = {
97
+ comments: [["<!--", "-->"]],
98
+ strings: [],
99
+ regions: [
100
+ ["<script", "</script>", clike],
101
+ ["<style", "</style>", css],
102
+ ],
103
+ };
104
+
105
+ const glimmerScript = {
106
+ comments: clike.comments,
107
+ strings: clike.strings,
108
+ regions: [["<template", "</template>", glimmer]],
109
+ };
110
+
111
+ const byExtension = {
112
+ js: clike,
113
+ jsx: clike,
114
+ mjs: clike,
115
+ cjs: clike,
116
+ ts: clike,
117
+ tsx: clike,
118
+ mts: clike,
119
+ cts: clike,
120
+ jsonc: clike,
121
+ json5: clike,
122
+ java: clike,
123
+ kt: clike,
124
+ kts: clike,
125
+ scala: clike,
126
+ groovy: clike,
127
+ c: clike,
128
+ h: clike,
129
+ cc: clike,
130
+ cpp: clike,
131
+ cxx: clike,
132
+ hpp: clike,
133
+ hh: clike,
134
+ m: clike,
135
+ mm: clike,
136
+ cs: clike,
137
+ go: clike,
138
+ swift: clike,
139
+ dart: clike,
140
+ php: clike,
141
+ zig: clike,
142
+ proto: clike,
143
+ gradle: clike,
144
+ rs: rust,
145
+ css,
146
+ scss: clike,
147
+ sass: clike,
148
+ less: clike,
149
+ py: python,
150
+ pyi: python,
151
+ rb: ruby,
152
+ rake: ruby,
153
+ sh: hash,
154
+ bash: hash,
155
+ zsh: hash,
156
+ fish: hash,
157
+ pl: hash,
158
+ pm: hash,
159
+ r: hash,
160
+ ex: hash,
161
+ exs: hash,
162
+ nim: hash,
163
+ ps1: hash,
164
+ yml: config,
165
+ yaml: config,
166
+ toml: config,
167
+ ini: config,
168
+ cfg: config,
169
+ conf: config,
170
+ env: config,
171
+ properties: config,
172
+ dockerfile: config,
173
+ makefile: config,
174
+ gitignore: config,
175
+ sql: dash,
176
+ hs: dash,
177
+ elm: dash,
178
+ ada: dash,
179
+ lua,
180
+ clj: lisp,
181
+ cljs: lisp,
182
+ edn: lisp,
183
+ lisp,
184
+ el: lisp,
185
+ scm: lisp,
186
+ rkt: lisp,
187
+ hbs: glimmer,
188
+ handlebars: glimmer,
189
+ html: html,
190
+ htm: html,
191
+ xml: html,
192
+ svg: html,
193
+ vue: html,
194
+ svelte: html,
195
+ astro: html,
196
+ gjs: glimmerScript,
197
+ gts: glimmerScript,
198
+ };
199
+
200
+ const proseExtensions = new Set([
201
+ "md",
202
+ "mdx",
203
+ "markdown",
204
+ "txt",
205
+ "text",
206
+ "rst",
207
+ "adoc",
208
+ "asciidoc",
209
+ "org",
210
+ ]);
211
+
212
+ const proseNames = new Set([
213
+ "readme",
214
+ "contributing",
215
+ "changelog",
216
+ "license",
217
+ "licence",
218
+ "authors",
219
+ "notice",
220
+ "todo",
221
+ "install",
222
+ "history",
223
+ "news",
224
+ ]);
225
+
226
+ const codeNames = {
227
+ dockerfile: config,
228
+ makefile: config,
229
+ gnumakefile: config,
230
+ rakefile: ruby,
231
+ gemfile: ruby,
232
+ guardfile: ruby,
233
+ vagrantfile: ruby,
234
+ brewfile: ruby,
235
+ justfile: hash,
236
+ procfile: config,
237
+ };
238
+
239
+ /**
240
+ * Returns { kind: "prose" } for text files,
241
+ * { kind: "code", language } for source files,
242
+ * and null for files this tool does not read.
243
+ */
244
+ export function classify(filePath) {
245
+ const base = filePath.slice(Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\")) + 1);
246
+ const lower = base.toLowerCase();
247
+ const dot = lower.lastIndexOf(".");
248
+
249
+ if (dot === -1) {
250
+ if (proseNames.has(lower)) return { kind: "prose" };
251
+ const language = codeNames[lower];
252
+ return language ? { kind: "code", language } : null;
253
+ }
254
+
255
+ const ext = lower.slice(dot + 1);
256
+ const stem = lower.slice(0, dot);
257
+
258
+ if (proseExtensions.has(ext)) return { kind: "prose" };
259
+ if (dot === 0) {
260
+ const language = byExtension[ext];
261
+ return language ? { kind: "code", language } : null;
262
+ }
263
+
264
+ const language = byExtension[ext] ?? codeNames[stem];
265
+ return language ? { kind: "code", language } : null;
266
+ }
package/src/lint.js ADDED
@@ -0,0 +1,51 @@
1
+ import { resolve } from "node:path";
2
+ import { Tinypool } from "tinypool";
3
+ import { findConfig, loadConfig } from "./config.js";
4
+ import { collectFiles } from "./files.js";
5
+
6
+ /**
7
+ * Checks every file under the given roots.
8
+ *
9
+ * onResult runs once per file, as soon as that file is done.
10
+ * The returned promise resolves after the last file.
11
+ */
12
+ export async function lint({
13
+ roots = ["."],
14
+ cwd = process.cwd(),
15
+ configPath,
16
+ git = true,
17
+ jobs,
18
+ onResult = () => {},
19
+ } = {}) {
20
+ const resolvedConfig = configPath ? resolve(cwd, configPath) : await findConfig(cwd);
21
+ const config = await loadConfig(resolvedConfig);
22
+ const files = await collectFiles(roots, { cwd, git, ignore: config.ignore });
23
+
24
+ const pool = new Tinypool({
25
+ filename: new URL("./worker.js", import.meta.url).href,
26
+ workerData: { configPath: resolvedConfig, cwd },
27
+ maxThreads: jobs,
28
+ });
29
+
30
+ const summary = { files: files.length, checked: 0, findings: 0, filesWithFindings: 0 };
31
+
32
+ try {
33
+ const pending = [];
34
+ for (let i = 0; i < files.length; i++) {
35
+ const done = pool.run(files[i]).then((result) => {
36
+ if (!result.skipped) summary.checked++;
37
+ if (result.findings.length > 0) {
38
+ summary.findings += result.findings.length;
39
+ summary.filesWithFindings++;
40
+ }
41
+ onResult(result);
42
+ });
43
+ pending.push(done);
44
+ }
45
+ await Promise.all(pending);
46
+ } finally {
47
+ await pool.destroy();
48
+ }
49
+
50
+ return summary;
51
+ }