Skip to content

Commit 203cf44

Browse files
committed
chore(sync): cascade hook tests + AI-attribution regex fix from socket-repo-template
Local prefer-undefined-over-null.js skip widening (a69d361) preserved.
1 parent 6b65f8c commit 203cf44

5 files changed

Lines changed: 1001 additions & 111 deletions

File tree

.git-hooks/_helpers.mts

Lines changed: 91 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,51 @@ export type LineHit = {
280280
suggested?: string
281281
}
282282

283+
// Generic line-walk scanner factory. Splits text into lines once,
284+
// applies the regex per line, optionally skips lines via `filter` (for
285+
// allowlists) and/or via `skipDocs` (for documentation-style
286+
// detection), and optionally attaches a suggested rewrite. Centralizes
287+
// the loop shape that every concrete scanner used to inline.
288+
//
289+
// Options:
290+
// filter — return true to drop a line (e.g. allowlist match).
291+
// skipDocs.rule — when set, calls looksLikeDocumentation() with the
292+
// same regex + this rule name and skips lines that match.
293+
// suggest — produces the per-line `suggested` rewrite shown to users.
294+
function scanLines(
295+
text: string,
296+
pattern: RegExp,
297+
options: {
298+
filter?: (line: string) => boolean
299+
skipDocs?: { rule: string }
300+
suggest?: (line: string) => string
301+
} = {},
302+
): LineHit[] {
303+
const hits: LineHit[] = []
304+
const lines = text.split('\n')
305+
for (let i = 0; i < lines.length; i++) {
306+
const line = lines[i]!
307+
if (!pattern.test(line)) {
308+
continue
309+
}
310+
if (options.filter && options.filter(line)) {
311+
continue
312+
}
313+
if (
314+
options.skipDocs &&
315+
looksLikeDocumentation(line, pattern, options.skipDocs.rule)
316+
) {
317+
continue
318+
}
319+
const hit: LineHit = { lineNumber: i + 1, line }
320+
if (options.suggest) {
321+
hit.suggested = options.suggest(line)
322+
}
323+
hits.push(hit)
324+
}
325+
return hits
326+
}
327+
283328
// Build a suggested rewrite for a documentation-style personal path.
284329
// Replaces the matched real-path username segment with the canonical
285330
// placeholder form: `<user>` / `<USERNAME>` (matching the platform
@@ -295,34 +340,23 @@ export function suggestPlaceholder(line: string): string {
295340
// are pure placeholders or look like documentation examples). Each hit
296341
// carries a `suggested` rewrite when the scanner can offer one — the
297342
// caller surfaces it to the user as the fix recipe.
298-
export const scanPersonalPaths = (text: string): LineHit[] => {
299-
const hits: LineHit[] = []
300-
const lines = text.split('\n')
301-
for (let i = 0; i < lines.length; i++) {
302-
const line = lines[i]!
303-
if (!PERSONAL_PATH_RE.test(line)) {
304-
continue
305-
}
306-
if (PERSONAL_PATH_PLACEHOLDER_RE.test(line)) {
343+
export const scanPersonalPaths = (text: string): LineHit[] =>
344+
scanLines(text, PERSONAL_PATH_RE, {
345+
filter: line => {
346+
// Pure-placeholder lines (no real path remains after stripping
347+
// every `<...>` placeholder) are documentation, not leaks.
348+
if (!PERSONAL_PATH_PLACEHOLDER_RE.test(line)) {
349+
return false
350+
}
307351
const stripped = line.replace(
308352
new RegExp(PERSONAL_PATH_PLACEHOLDER_RE, 'g'),
309353
'',
310354
)
311-
if (!PERSONAL_PATH_RE.test(stripped)) {
312-
continue
313-
}
314-
}
315-
if (looksLikeDocumentation(line, PERSONAL_PATH_RE, 'personal-path')) {
316-
continue
317-
}
318-
hits.push({
319-
lineNumber: i + 1,
320-
line,
321-
suggested: suggestPlaceholder(line),
322-
})
323-
}
324-
return hits
325-
}
355+
return !PERSONAL_PATH_RE.test(stripped)
356+
},
357+
skipDocs: { rule: 'personal-path' },
358+
suggest: suggestPlaceholder,
359+
})
326360

327361
// ── Secret scanners ────────────────────────────────────────────────
328362

@@ -331,53 +365,17 @@ const AWS_KEY_RE = /(aws_access_key|aws_secret|\bAKIA[0-9A-Z]{16}\b)/i
331365
const GITHUB_TOKEN_RE = /gh[ps]_[a-zA-Z0-9]{36}/
332366
const PRIVATE_KEY_RE = /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/
333367

334-
export const scanSocketApiKeys = (text: string): LineHit[] => {
335-
const hits: LineHit[] = []
336-
const lines = text.split('\n')
337-
for (let i = 0; i < lines.length; i++) {
338-
const line = lines[i]!
339-
if (SOCKET_API_KEY_RE.test(line) && !isAllowedApiKey(line)) {
340-
hits.push({ lineNumber: i + 1, line })
341-
}
342-
}
343-
return hits
344-
}
368+
export const scanSocketApiKeys = (text: string): LineHit[] =>
369+
scanLines(text, SOCKET_API_KEY_RE, { filter: isAllowedApiKey })
345370

346-
export const scanAwsKeys = (text: string): LineHit[] => {
347-
const hits: LineHit[] = []
348-
const lines = text.split('\n')
349-
for (let i = 0; i < lines.length; i++) {
350-
const line = lines[i]!
351-
if (AWS_KEY_RE.test(line)) {
352-
hits.push({ lineNumber: i + 1, line })
353-
}
354-
}
355-
return hits
356-
}
371+
export const scanAwsKeys = (text: string): LineHit[] =>
372+
scanLines(text, AWS_KEY_RE)
357373

358-
export const scanGitHubTokens = (text: string): LineHit[] => {
359-
const hits: LineHit[] = []
360-
const lines = text.split('\n')
361-
for (let i = 0; i < lines.length; i++) {
362-
const line = lines[i]!
363-
if (GITHUB_TOKEN_RE.test(line)) {
364-
hits.push({ lineNumber: i + 1, line })
365-
}
366-
}
367-
return hits
368-
}
374+
export const scanGitHubTokens = (text: string): LineHit[] =>
375+
scanLines(text, GITHUB_TOKEN_RE)
369376

370-
export const scanPrivateKeys = (text: string): LineHit[] => {
371-
const hits: LineHit[] = []
372-
const lines = text.split('\n')
373-
for (let i = 0; i < lines.length; i++) {
374-
const line = lines[i]!
375-
if (PRIVATE_KEY_RE.test(line)) {
376-
hits.push({ lineNumber: i + 1, line })
377-
}
378-
}
379-
return hits
380-
}
377+
export const scanPrivateKeys = (text: string): LineHit[] =>
378+
scanLines(text, PRIVATE_KEY_RE)
381379

382380
// ── npx/dlx scanner ────────────────────────────────────────────────
383381
//
@@ -407,34 +405,24 @@ const NPX_DLX_RE = /(?<![\w\-:=.])\b(npx|yarn dlx)\b(?![\w\-:=.])/
407405
// looksLikeDocumentation(); we only ever land here for code lines, where
408406
// the right swap is `pnpm exec` (since `pnpm` is the fleet's package
409407
// manager) or `pnpm run` for script entries. For documentation lines
410-
// that legitimately need a fetch-and-run command (user-facing
411-
// instructions where the consumer doesn't have the package pinned),
412-
// use `pnpm dlx` or its pnpm v11 shorthand `pnx` instead of `npx`.
408+
// All dlx-style invocations rewrite to `pnpm exec`. This matches the
409+
// `socket/no-npx-dlx` oxlint rule's autofix and the CLAUDE.md tooling
410+
// rule (NEVER use npx / pnpm dlx / yarn dlx — use pnpm exec). Keep
411+
// the alternation ordered longest-prefix-first so `pnpm dlx` matches
412+
// before any future `pnpm`-anchored rule could shadow it.
413413
export function suggestNpxReplacement(line: string): string {
414414
return line
415+
.replace(/\bpnpm dlx\b/g, 'pnpm exec')
415416
.replace(/\byarn dlx\b/g, 'pnpm exec')
416-
.replace(/\bnpx\b/g, 'pnpm dlx')
417+
.replace(/\bpnx\b/g, 'pnpm exec')
418+
.replace(/\bnpx\b/g, 'pnpm exec')
417419
}
418420

419-
export const scanNpxDlx = (text: string): LineHit[] => {
420-
const hits: LineHit[] = []
421-
const lines = text.split('\n')
422-
for (let i = 0; i < lines.length; i++) {
423-
const line = lines[i]!
424-
if (!NPX_DLX_RE.test(line)) {
425-
continue
426-
}
427-
if (looksLikeDocumentation(line, NPX_DLX_RE, 'npx')) {
428-
continue
429-
}
430-
hits.push({
431-
lineNumber: i + 1,
432-
line,
433-
suggested: suggestNpxReplacement(line),
434-
})
435-
}
436-
return hits
437-
}
421+
export const scanNpxDlx = (text: string): LineHit[] =>
422+
scanLines(text, NPX_DLX_RE, {
423+
skipDocs: { rule: 'npx' },
424+
suggest: suggestNpxReplacement,
425+
})
438426

439427
// ── Logger leak scanner ────────────────────────────────────────────
440428
//
@@ -464,25 +452,11 @@ export function suggestLoggerReplacement(line: string): string {
464452
.replace(/\bconsole\.log\s*\(/g, 'logger.info(')
465453
}
466454

467-
export const scanLoggerLeaks = (text: string): LineHit[] => {
468-
const hits: LineHit[] = []
469-
const lines = text.split('\n')
470-
for (let i = 0; i < lines.length; i++) {
471-
const line = lines[i]!
472-
if (!LOGGER_LEAK_RE.test(line)) {
473-
continue
474-
}
475-
if (looksLikeDocumentation(line, LOGGER_LEAK_RE, 'console')) {
476-
continue
477-
}
478-
hits.push({
479-
lineNumber: i + 1,
480-
line,
481-
suggested: suggestLoggerReplacement(line),
482-
})
483-
}
484-
return hits
485-
}
455+
export const scanLoggerLeaks = (text: string): LineHit[] =>
456+
scanLines(text, LOGGER_LEAK_RE, {
457+
skipDocs: { rule: 'console' },
458+
suggest: suggestLoggerReplacement,
459+
})
486460

487461
// ── Cross-repo path scanner ────────────────────────────────────────
488462
//
@@ -566,9 +540,15 @@ export const scanCrossRepoPaths = (
566540
}
567541

568542
// ── AI attribution scanner ─────────────────────────────────────────
543+
//
544+
// Matches BOILERPLATE attribution patterns ("Generated with Claude",
545+
// "Co-Authored-By: Claude", emoji prefixes, vendor email addresses) —
546+
// not legitimate product / directory references. Bare "Claude" /
547+
// "Claude Code" / ".claude/" are valid prose; only the
548+
// attribution-verb-anchored forms trigger the hook.
569549

570550
const AI_ATTRIBUTION_RE =
571-
/(Generated with.*(Claude|AI)|Co-Authored-By: Claude|Co-Authored-By: AI|🤖 Generated|AI generated|@anthropic\.com|Assistant:|Generated by Claude|Machine generated|Claude Code)/i
551+
/(?:(?:Generated|Built|Created|Made|Written|Authored|Powered|Crafted)\s+(?:with|by)\s+(?:Claude|AI|GPT|ChatGPT|Copilot|Cursor|Bard|Gemini)|Co-Authored-By:\s+(?:Claude|AI|GPT|ChatGPT|Copilot|Cursor|Bard|Gemini)|🤖\s+Generated|AI[\s-]generated|Machine[\s-]generated|@(?:anthropic|openai)\.com|^Assistant:)/im
572552

573553
export const containsAiAttribution = (text: string): boolean =>
574554
AI_ATTRIBUTION_RE.test(text)

0 commit comments

Comments
 (0)