@codesema/speccov 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, relative, resolve, sep } from 'node:path';
3
+ import { climbFor } from './climb';
4
+
5
+ export const IGNORE_FILE = '.speccovignore';
6
+
7
+ /** Says whether a path was set aside by the project. */
8
+ export type Ignore = (path: string) => boolean;
9
+
10
+ /**
11
+ * The perimeter the project declares, read from the nearest `.speccovignore`.
12
+ *
13
+ * A test harness or a browser-side client is real code, but not where a value enters the
14
+ * system: its sites drown the ones that matter. Nothing can tell that from the code alone
15
+ * without guessing by name, so the project says it, in a versioned file, and the report
16
+ * counts what was left out instead of keeping quiet about it.
17
+ *
18
+ * Patterns are relative to the directory of the file, as in a `.gitignore`: the same line
19
+ * holds whether the scan aims at the repository or at one of its directories.
20
+ */
21
+ export async function loadIgnore(root: string): Promise<Ignore> {
22
+ const file = climbFor(IGNORE_FILE, root);
23
+ if (file === undefined) {
24
+ return () => false;
25
+ }
26
+ const matches = parseIgnore(await readFile(file, 'utf8'));
27
+ const base = dirname(file);
28
+ return (path) => matches(relative(base, resolve(path)).split(sep).join('/'));
29
+ }
30
+
31
+ /**
32
+ * The subset of the `.gitignore` syntax a perimeter needs: `*`, `?`, `**`, comments, a
33
+ * leading `/` to anchor, and a line without a slash matching at any depth. A directory
34
+ * takes everything under it.
35
+ */
36
+ export function parseIgnore(content: string): Ignore {
37
+ const patterns = content
38
+ .split('\n')
39
+ .map((line) => line.trim())
40
+ .filter((line) => line !== '' && !line.startsWith('#'))
41
+ .flatMap(toPatterns);
42
+ return (path) => patterns.some((pattern) => pattern.test(path));
43
+ }
44
+
45
+ function toPatterns(line: string): readonly RegExp[] {
46
+ // Read as a plain pattern, a negation would set aside the very files it means to keep.
47
+ if (line.startsWith('!')) {
48
+ throw new Error(`speccov: ${IGNORE_FILE} does not support negation ("${line}"). Narrow the pattern above it instead.`);
49
+ }
50
+ const path = line.replace(/\/$/u, '');
51
+ const glob = path.includes('/') ? path.replace(/^\//u, '') : `**/${path}`;
52
+ return [toRegExp(glob), toRegExp(`${glob}/**`)];
53
+ }
54
+
55
+ const GLOB = new Map([
56
+ ['**/', '(?:.*/)?'],
57
+ ['**', '.*'],
58
+ ['*', '[^/]*'],
59
+ ['?', '[^/]'],
60
+ ]);
61
+
62
+ function toRegExp(glob: string): RegExp {
63
+ const source = glob.replaceAll(/\*\*\/|\*\*|\*|\?|[.+^${}()|[\]\\]/gu, (token) => GLOB.get(token) ?? `\\${token}`);
64
+ return new RegExp(`^${source}$`, 'u');
65
+ }