@zgeoff/chex 0.0.1 → 0.0.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 +96 -3
- package/lefthook.yml +123 -0
- package/package.json +31 -3
- package/src/build-init-package.ts +57 -0
- package/src/build-init-plan.ts +96 -0
- package/src/build-lefthook-extends.ts +117 -0
- package/src/check-names.ts +18 -0
- package/src/cli.ts +49 -0
- package/src/collect-command-names.ts +297 -0
- package/src/collect-expression-references.ts +17 -0
- package/src/dependency-gates/collect-stale-age-gate-excludes.ts +83 -0
- package/src/dependency-gates/collect-unneeded-audit-ignores.ts +41 -0
- package/src/dependency-gates/collect-unneeded-overrides.ts +70 -0
- package/src/dependency-gates/first-party-scopes.ts +1 -0
- package/src/dependency-gates/is-record.ts +4 -0
- package/src/dependency-gates/parse-audit-advisories.ts +33 -0
- package/src/dependency-gates/parse-audit-ignore-ids.ts +15 -0
- package/src/dependency-gates/parse-fixable-packages.ts +29 -0
- package/src/dependency-gates/parse-lockfile-dependents.ts +77 -0
- package/src/dependency-gates/parse-root-manifest.ts +68 -0
- package/src/dependency-gates/read-dependency-gate-findings.ts +91 -0
- package/src/dependency-gates/run-dependency-gates-check.ts +54 -0
- package/src/dependency-gates/types.ts +19 -0
- package/src/docs/check-length.sh +117 -0
- package/src/docs/check-links.sh +245 -0
- package/src/docs/check-prose.sh +657 -0
- package/src/docs/check-readmes.sh +102 -0
- package/src/docs/check-skill.sh +413 -0
- package/src/docs/check-vision.sh +34 -0
- package/src/find-install-cache-path.ts +16 -0
- package/src/format-commit-lines.ts +3 -0
- package/src/make-check-runner.ts +40 -0
- package/src/parse-added-lines.ts +112 -0
- package/src/parse-chex-args.ts +76 -0
- package/src/parse-lane-limits.ts +59 -0
- package/src/parse-pushed-refs.ts +29 -0
- package/src/parse-shell-words.ts +520 -0
- package/src/pick-merge-blobs-command.ts +152 -0
- package/src/read-branch-base.ts +31 -0
- package/src/read-branch-files.ts +59 -0
- package/src/read-pushed-ranges.ts +47 -0
- package/src/read-pushed-refs-input.ts +18 -0
- package/src/read-range-commits.ts +30 -0
- package/src/run-chex.ts +62 -0
- package/src/run-commits-check.ts +178 -0
- package/src/run-init.ts +129 -0
- package/src/run-local-paths-check.ts +185 -0
- package/src/run-program.ts +38 -0
- package/src/run-test-timeouts-check.ts +498 -0
- package/src/run-uncommitted-check.ts +82 -0
- package/src/run-workflows-check.ts +610 -0
- package/src/shell-escape-parsers.ts +197 -0
- package/src/write-text-file.ts +7 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
interface ShellEscape {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly length: number;
|
|
4
|
+
readonly isQuoting: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ShellEscapeParser = (command: string, index: number) => ShellEscape;
|
|
8
|
+
|
|
9
|
+
// One parser for each escape rule of the Bash manual's Quoting section, typed over every rule, so a
|
|
10
|
+
// rule without a parser fails the type check.
|
|
11
|
+
export const shellEscapeParsers: Readonly<
|
|
12
|
+
Record<
|
|
13
|
+
'code' | 'backtick' | 'quoted-backtick' | 'literal' | 'double-quote' | 'ansi-c',
|
|
14
|
+
ShellEscapeParser
|
|
15
|
+
>
|
|
16
|
+
> = {
|
|
17
|
+
code: parseCodeEscape,
|
|
18
|
+
backtick: makeBacktickEscapeParser('$`\\'),
|
|
19
|
+
'quoted-backtick': makeBacktickEscapeParser('$`\\"'),
|
|
20
|
+
literal: parseLiteralEscape,
|
|
21
|
+
'double-quote': parseDoubleQuotedEscape,
|
|
22
|
+
'ansi-c': parseANSICEscape,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Outside quotes, a backslash keeps the next character literal, and before a newline it joins the
|
|
26
|
+
// two lines.
|
|
27
|
+
function parseCodeEscape(command: string, index: number): ShellEscape {
|
|
28
|
+
const escaped = command.charAt(index + 1);
|
|
29
|
+
|
|
30
|
+
if (escaped === '') {
|
|
31
|
+
return { text: '\\', length: 1, isQuoting: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return escaped === '\n'
|
|
35
|
+
? { text: '', length: 2, isQuoting: false }
|
|
36
|
+
: { text: escaped, length: 2, isQuoting: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The shell reads a backtick substitution twice. The first read turns a backslash and a character
|
|
40
|
+
// in `escapable` into that character, and the second read parses the result as code.
|
|
41
|
+
function makeBacktickEscapeParser(escapable: string): ShellEscapeParser {
|
|
42
|
+
return (command, index) => {
|
|
43
|
+
const first = readFirstPass(command, index, escapable);
|
|
44
|
+
|
|
45
|
+
if (first.length === 1) {
|
|
46
|
+
return parseCodeEscape(command, index);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A nested backtick needs both reads to open, so it stays text.
|
|
50
|
+
if (first.text === '`') {
|
|
51
|
+
return { text: '`', length: 2, isQuoting: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (first.text !== '\\') {
|
|
55
|
+
return { text: '', length: 1, isQuoting: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const next = readFirstPass(command, index + 2, escapable);
|
|
59
|
+
|
|
60
|
+
if (next.text === '') {
|
|
61
|
+
return { text: '\\', length: 2, isQuoting: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return next.text === '\n'
|
|
65
|
+
? { text: '', length: 2 + next.length, isQuoting: false }
|
|
66
|
+
: { text: next.text, length: 2 + next.length, isQuoting: true };
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readFirstPass(
|
|
71
|
+
command: string,
|
|
72
|
+
index: number,
|
|
73
|
+
escapable: string,
|
|
74
|
+
): { readonly text: string; readonly length: number } {
|
|
75
|
+
const character = command.charAt(index);
|
|
76
|
+
const escaped = command.charAt(index + 1);
|
|
77
|
+
|
|
78
|
+
return character === '\\' && escaped !== '' && escapable.includes(escaped)
|
|
79
|
+
? { text: escaped, length: 2 }
|
|
80
|
+
: { text: character, length: 1 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Single quotes hold no escape, so a backslash there is text.
|
|
84
|
+
function parseLiteralEscape(): ShellEscape {
|
|
85
|
+
return { text: '\\', length: 1, isQuoting: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const doubleQuotedEscapable = '$`"\\\n';
|
|
89
|
+
|
|
90
|
+
// Inside double quotes, a backslash escapes only `$`, a backtick, `"`, `\` and a newline, and stays
|
|
91
|
+
// before any other character.
|
|
92
|
+
function parseDoubleQuotedEscape(command: string, index: number): ShellEscape {
|
|
93
|
+
const escaped = command.charAt(index + 1);
|
|
94
|
+
|
|
95
|
+
if (escaped === '' || !doubleQuotedEscapable.includes(escaped)) {
|
|
96
|
+
return { text: '\\', length: 1, isQuoting: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { text: escaped === '\n' ? '' : escaped, length: 2, isQuoting: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ansiCEscapes = new Map([
|
|
103
|
+
['a', '\u0007'],
|
|
104
|
+
['b', '\b'],
|
|
105
|
+
['e', '\u001B'],
|
|
106
|
+
['E', '\u001B'],
|
|
107
|
+
['f', '\f'],
|
|
108
|
+
['n', '\n'],
|
|
109
|
+
['r', '\r'],
|
|
110
|
+
['t', '\t'],
|
|
111
|
+
['v', '\v'],
|
|
112
|
+
['\\', '\\'],
|
|
113
|
+
["'", "'"],
|
|
114
|
+
['"', '"'],
|
|
115
|
+
['?', '?'],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const ansiCBytePattern = /^\\(?:(?<octal>[0-7]{1,3})|x(?<hex>[\dA-Fa-f]{1,2}))/u;
|
|
119
|
+
|
|
120
|
+
const ansiCCodePattern =
|
|
121
|
+
/^\\(?:u(?<unicode>[\dA-Fa-f]{1,4})|U(?<wide>[\dA-Fa-f]{1,8})|c(?<control>[\s\S]))/u;
|
|
122
|
+
|
|
123
|
+
// Inside `$'...'`, a backslash starts one of the escapes the Bash manual lists, and an unknown one
|
|
124
|
+
// keeps its backslash. Bash ends the expansion at a NUL, so the rest of the quote drops out.
|
|
125
|
+
function parseANSICEscape(command: string, index: number): ShellEscape {
|
|
126
|
+
const named = ansiCEscapes.get(command.charAt(index + 1));
|
|
127
|
+
|
|
128
|
+
const escape =
|
|
129
|
+
named === undefined
|
|
130
|
+
? parseANSICNumber(command, index)
|
|
131
|
+
: { text: named, length: 2, isQuoting: false };
|
|
132
|
+
|
|
133
|
+
const end = escape?.text.indexOf('\0') ?? -1;
|
|
134
|
+
|
|
135
|
+
if (escape === undefined) {
|
|
136
|
+
return { text: '\\', length: 1, isQuoting: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return end === -1
|
|
140
|
+
? escape
|
|
141
|
+
: {
|
|
142
|
+
text: escape.text.slice(0, end),
|
|
143
|
+
length: getANSICQuoteEnd(command, index + escape.length) - index,
|
|
144
|
+
isQuoting: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Octal and hexadecimal escapes each give one byte, so a run of them decodes as UTF-8 together.
|
|
149
|
+
function parseANSICNumber(command: string, index: number): ShellEscape | undefined {
|
|
150
|
+
const bytes: number[] = [];
|
|
151
|
+
let length = 0;
|
|
152
|
+
let match = ansiCBytePattern.exec(command.slice(index));
|
|
153
|
+
|
|
154
|
+
while (match?.groups !== undefined) {
|
|
155
|
+
const octal = match.groups['octal'];
|
|
156
|
+
|
|
157
|
+
bytes.push(
|
|
158
|
+
(octal === undefined
|
|
159
|
+
? Number.parseInt(match.groups['hex'] ?? '', 16)
|
|
160
|
+
: Number.parseInt(octal, 8)) & 0xff,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
length += match[0].length;
|
|
164
|
+
match = ansiCBytePattern.exec(command.slice(index + length));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (bytes.length > 0) {
|
|
168
|
+
return { text: new TextDecoder().decode(new Uint8Array(bytes)), length, isQuoting: false };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const code = ansiCCodePattern.exec(command.slice(index, index + 10));
|
|
172
|
+
|
|
173
|
+
return code?.groups === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: { text: parseANSICCode(code.groups), length: code[0].length, isQuoting: false };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseANSICCode(groups: Readonly<Record<string, string | undefined>>): string {
|
|
179
|
+
const control = groups['control'];
|
|
180
|
+
const point = Number.parseInt(groups['unicode'] ?? groups['wide'] ?? '', 16);
|
|
181
|
+
|
|
182
|
+
if (control !== undefined) {
|
|
183
|
+
return String.fromCodePoint((control.toUpperCase().codePointAt(0) ?? 0) ^ 64);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return point <= 0x10_ff_ff ? String.fromCodePoint(point) : '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getANSICQuoteEnd(command: string, start: number): number {
|
|
190
|
+
let index = start;
|
|
191
|
+
|
|
192
|
+
while (index < command.length && command.charAt(index) !== "'") {
|
|
193
|
+
index += command.charAt(index) === '\\' ? 2 : 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return Math.min(index, command.length);
|
|
197
|
+
}
|