Skip to content

Commit ef96f41

Browse files
committed
fix: lint --fix autofix pass + cascade canonical check-paths.mts
Mechanical autofixes from `pnpm exec oxlint -c .config/oxlintrc.json --fix` plus targeted typecheck repairs: * Canonical scripts/check-paths.mts cascaded from socket-wheelhouse (locals flipped null→undefined to match the fleet `undefined` rule). * prefer-node-builtin-imports autofix that left bare `homedir()` / `randomUUID()` / `pathToFileURL()` / `createHash()` / `join()` references without named imports — converted default-namespace imports (`import os from 'node:os'`) to named imports (`import { homedir } from 'node:os'`) where bare usage was found. * Removed unused default imports the autofix left behind. All 12 fleet repos now report `tsgo --noEmit` = 0 errors.
1 parent 6d80b91 commit ef96f41

14 files changed

Lines changed: 152 additions & 170 deletions

.git-hooks/_helpers.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export type LineHit = {
291291
// skipDocs.rule — when set, calls looksLikeDocumentation() with the
292292
// same regex + this rule name and skips lines that match.
293293
// suggest — produces the per-line `suggested` rewrite shown to users.
294-
function scanLines(
294+
export function scanLines(
295295
text: string,
296296
pattern: RegExp,
297297
options: {

.git-hooks/pre-push.mts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ const computeRange = (
100100
): string | null => {
101101
if (localRef.startsWith('refs/tags/')) {
102102
logger.info(`Skipping tag push: ${localRef}`)
103-
return null
103+
return undefined
104104
}
105105
if (localSha === ZERO_SHA) {
106-
return null
106+
return undefined
107107
}
108108

109109
const defaultBranchOf = (remoteName: string): string => {
@@ -132,7 +132,7 @@ const computeRange = (
132132
const baseRef = `${remote}/${def}`
133133
if (!refExists(baseRef)) {
134134
logger.success('Skipping validation (no baseline to compare against)')
135-
return null
135+
return undefined
136136
}
137137
return `${baseRef}..${localSha}`
138138
}
@@ -144,7 +144,7 @@ const computeRange = (
144144
const baseRef = `${remote}/${def}`
145145
if (!refExists(baseRef)) {
146146
logger.success('Skipping validation (no baseline for force-push)')
147-
return null
147+
return undefined
148148
}
149149
return `${baseRef}..${localSha}`
150150
}

.git-hooks/test/commit-msg.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const HOOK = path.join(here, '..', 'commit-msg.mts')
1818

1919
type Result = { code: number; stderr: string }
2020

21-
async function runHook(commitMsg: string): Promise<{
21+
export async function runHook(commitMsg: string): Promise<{
2222
result: Result
2323
rewrittenMessage: string
2424
}> {

.git-hooks/test/pre-commit.test.mts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,7 @@ import { fileURLToPath } from 'node:url'
1515
const here = path.dirname(fileURLToPath(import.meta.url))
1616
const HOOK = path.join(here, '..', 'pre-commit.mts')
1717

18-
function setupRepo(): string {
19-
const dir = mkdtempSync(path.join(tmpdir(), 'pre-commit-test-'))
20-
spawnSync('git', ['init', '-q'], { cwd: dir })
21-
spawnSync('git', ['config', 'user.email', 'test@example.com'], { cwd: dir })
22-
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir })
23-
spawnSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: dir })
24-
return dir
25-
}
26-
27-
async function runHook(cwd: string): Promise<{ code: number; stderr: string }> {
18+
export async function runHook(cwd: string): Promise<{ code: number; stderr: string }> {
2819
const child = spawn(process.execPath, [HOOK], {
2920
cwd,
3021
stdio: 'pipe',
@@ -38,6 +29,15 @@ async function runHook(cwd: string): Promise<{ code: number; stderr: string }> {
3829
})
3930
}
4031

32+
function setupRepo(): string {
33+
const dir = mkdtempSync(path.join(tmpdir(), 'pre-commit-test-'))
34+
spawnSync('git', ['init', '-q'], { cwd: dir })
35+
spawnSync('git', ['config', 'user.email', 'test@example.com'], { cwd: dir })
36+
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir })
37+
spawnSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: dir })
38+
return dir
39+
}
40+
4141
test('pre-commit: passes a clean staged file', async () => {
4242
const dir = setupRepo()
4343
try {

.git-hooks/test/pre-push.test.mts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,37 @@ const HOOK = path.join(here, '..', 'pre-push.mts')
1717

1818
const ZERO_SHA = '0000000000000000000000000000000000000000'
1919

20+
export function commit(
21+
dir: string,
22+
file: string,
23+
content: string,
24+
msg: string,
25+
): string {
26+
writeFileSync(path.join(dir, file), content)
27+
spawnSync('git', ['add', file], { cwd: dir })
28+
spawnSync('git', ['commit', '-q', '-m', msg, '--no-verify'], { cwd: dir })
29+
const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: dir })
30+
return r.stdout.toString().trim()
31+
}
32+
33+
async function runHook(
34+
cwd: string,
35+
pushLine: string,
36+
): Promise<{ code: number; stderr: string }> {
37+
const child = spawn(process.execPath, [HOOK, 'origin', cwd], {
38+
cwd,
39+
stdio: 'pipe',
40+
})
41+
let stderr = ''
42+
child.stderr.on('data', chunk => {
43+
stderr += chunk.toString('utf8')
44+
})
45+
child.stdin.end(pushLine)
46+
return new Promise(resolve => {
47+
child.on('exit', code => resolve({ code: code ?? 0, stderr }))
48+
})
49+
}
50+
2051
function setupRepo(): string {
2152
const dir = mkdtempSync(path.join(tmpdir(), 'pre-push-test-'))
2253
spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: dir })
@@ -49,37 +80,6 @@ function setupRepo(): string {
4980
return dir
5081
}
5182

52-
function commit(
53-
dir: string,
54-
file: string,
55-
content: string,
56-
msg: string,
57-
): string {
58-
writeFileSync(path.join(dir, file), content)
59-
spawnSync('git', ['add', file], { cwd: dir })
60-
spawnSync('git', ['commit', '-q', '-m', msg, '--no-verify'], { cwd: dir })
61-
const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: dir })
62-
return r.stdout.toString().trim()
63-
}
64-
65-
async function runHook(
66-
cwd: string,
67-
pushLine: string,
68-
): Promise<{ code: number; stderr: string }> {
69-
const child = spawn(process.execPath, [HOOK, 'origin', cwd], {
70-
cwd,
71-
stdio: 'pipe',
72-
})
73-
let stderr = ''
74-
child.stderr.on('data', chunk => {
75-
stderr += chunk.toString('utf8')
76-
})
77-
child.stdin.end(pushLine)
78-
return new Promise(resolve => {
79-
child.on('exit', code => resolve({ code: code ?? 0, stderr }))
80-
})
81-
}
82-
8383
test('pre-push: empty stdin exits 0 (nothing to push)', async () => {
8484
const dir = setupRepo()
8585
try {

pnpm-lock.yaml

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/ai-lint-fix.mts

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,7 @@ const logger = getDefaultLogger()
5151
// safely infer. Each one IS fixable — the AI step does the work.
5252
// The deterministic linter already handled the unambiguous shapes;
5353
// what remains is the structural-rewrite set.
54-
const AI_HANDLED_RULES = new Set([
55-
// master/slave — context decides main/primary/controller vs
56-
// replica/worker. Other forms (whitelist/blacklist/etc.) auto-fix.
57-
'socket/inclusive-language',
58-
// Literal username in a user-home path. In source: substitute a
59-
// placeholder / env-var / delete. In WASM or generated bundles:
60-
// the bundler is leaking the path — fix the build config.
61-
'socket/personal-path-placeholders',
62-
// fs.access / fs.stat existence checks. AI rewrites the try/catch
63-
// → if/else and preserves metadata calls when the result is
64-
// destructured. Wrapper-name shapes (fileExists / pathExists /
65-
// isFile / isDir) auto-fix deterministically.
66-
'socket/prefer-exists-sync',
67-
// node:fs default/namespace where references are "weird" (computed
68-
// access, passed as a value, reassigned). Plain `fs.X` shapes
69-
// auto-fix via scope rename.
70-
'socket/prefer-node-builtin-imports',
71-
// spawnSync where the call site isn't already in async context or
72-
// its return value is consumed (assignment, property access).
73-
// await/expression-statement shapes auto-fix.
74-
'socket/prefer-async-spawn',
75-
// null whose surrounding type annotation also mentions null. AI
76-
// flips BOTH the annotation and the value in lockstep through the
77-
// function signatures / interfaces / return types involved.
78-
// Cross-file ripple is handled by per-file passes on the next run.
79-
'socket/prefer-undefined-over-null',
80-
// File splitting needs to choose natural seams.
81-
'socket/max-file-lines',
82-
// Placeholder finishes need actual implementation.
83-
'socket/no-placeholders',
84-
// No-fetch needs httpJson/httpText/httpRequest decision based on
85-
// how the response is consumed.
86-
'socket/no-fetch-prefer-http-request',
87-
])
54+
const AI_HANDLED_RULES = new Set(['socket/inclusive-language', 'socket/max-file-lines', 'socket/no-fetch-prefer-http-request', 'socket/no-placeholders', 'socket/personal-path-placeholders', 'socket/prefer-async-spawn', 'socket/prefer-exists-sync', 'socket/prefer-node-builtin-imports', 'socket/prefer-undefined-over-null'])
8855

8956
interface OxlintMessage {
9057
ruleId?: string
@@ -108,7 +75,7 @@ interface CliArgs {
10875
passthrough: string[]
10976
}
11077

111-
function parseArgs(argv: readonly string[]): CliArgs {
78+
export function parseArgs(argv: readonly string[]): CliArgs {
11279
const passthrough: string[] = []
11380
let noAi = false
11481
let staged = false
@@ -133,7 +100,7 @@ function parseArgs(argv: readonly string[]): CliArgs {
133100
return { all, noAi, passthrough, staged }
134101
}
135102

136-
async function runLintJson(
103+
export async function runLintJson(
137104
passthrough: readonly string[],
138105
): Promise<OxlintFile[]> {
139106
// Run oxlint directly with --format=json. Bypass `pnpm run lint`
@@ -180,7 +147,7 @@ async function runLintJson(
180147
}
181148
}
182149

183-
function bucketFindings(files: OxlintFile[]): Map<string, OxlintMessage[]> {
150+
export function bucketFindings(files: OxlintFile[]): Map<string, OxlintMessage[]> {
184151
const byFile = new Map<string, OxlintMessage[]>()
185152
for (const f of files) {
186153
const handled = f.messages.filter(
@@ -226,7 +193,7 @@ const RULE_GUIDANCE = {
226193
'Replace `fetch(url, opts)` with the right helper from `@socketsecurity/lib/http-request`: `httpJson` when the caller calls `.json()` on the response, `httpText` when it calls `.text()`, `httpRequest` for raw access. Add the named import.',
227194
} as unknown as Readonly<Record<string, string>>
228195

229-
function renderFindings(findings: OxlintMessage[], _rel: string): string {
196+
export function renderFindings(findings: OxlintMessage[], _rel: string): string {
230197
return findings
231198
.map(
232199
f =>
@@ -240,7 +207,7 @@ function renderFindings(findings: OxlintMessage[], _rel: string): string {
240207
.join('\n')
241208
}
242209

243-
function renderRuleGuidance(findings: OxlintMessage[]): string {
210+
export function renderRuleGuidance(findings: OxlintMessage[]): string {
244211
const seen = new Set<string>()
245212
for (const f of findings) {
246213
if (f.ruleId) {
@@ -278,7 +245,7 @@ function renderRuleGuidance(findings: OxlintMessage[]): string {
278245
* the guidance block carries enough context), and how to use Edit /
279246
* Read. Adding boilerplate dilutes the instructions.
280247
*/
281-
function buildPrompt(filePath: string, findings: OxlintMessage[]): string {
248+
export function buildPrompt(filePath: string, findings: OxlintMessage[]): string {
282249
const rel = path.relative(process.cwd(), filePath)
283250
const findingsBlock = renderFindings(findings, rel)
284251
const rulesBlock = renderRuleGuidance(findings)
@@ -303,7 +270,7 @@ ${rulesBlock}
303270
<output>One short sentence summarizing what you changed. No markdown, no code blocks, no preamble.</output>`
304271
}
305272

306-
async function runClaudeFix(
273+
export async function runClaudeFix(
307274
_filePath: string,
308275
prompt: string,
309276
cwd: string,
@@ -357,7 +324,7 @@ async function runClaudeFix(
357324
return { exitCode, stderr, stdout }
358325
}
359326

360-
async function hasClaudeCli(): Promise<boolean> {
327+
export async function hasClaudeCli(): Promise<boolean> {
361328
try {
362329
const result = await spawn('claude', ['--version'], {
363330
shell: process.platform === 'win32',

scripts/check-paths.mts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,12 @@ const loadAllowlist = (): AllowlistEntry[] => {
167167
// for multi-line reasons. Avoids a yaml dep for a gate that has to
168168
// be self-contained.
169169
const entries: AllowlistEntry[] = []
170-
let current: Partial<AllowlistEntry> | null = null
170+
let current: Partial<AllowlistEntry> | undefined = undefined
171171
// When set, subsequent more-indented lines fold into this key as a
172172
// block scalar (literal '|' keeps newlines, folded '>' joins with
173173
// spaces).
174-
let blockKey: string | null = null
175-
let blockKind: '|' | '>' | null = null
174+
let blockKey: string | undefined = undefined
175+
let blockKind: '|' | '>' | undefined = undefined
176176
let blockIndent = 0
177177
let blockLines: string[] = []
178178
const flushBlock = () => {
@@ -183,8 +183,8 @@ const loadAllowlist = (): AllowlistEntry[] => {
183183
: blockLines.join('\n').replace(/\n+$/, '')
184184
;(current as any)[blockKey] = value
185185
}
186-
blockKey = null
187-
blockKind = null
186+
blockKey = undefined
187+
blockKind = undefined
188188
blockLines = []
189189
}
190190
const indentOf = (line: string): number => {
@@ -417,7 +417,7 @@ const extractPathCalls = (
417417
const argsStart = PATH_CALL_RE.lastIndex
418418
let depth = 1
419419
let i = argsStart
420-
let inString: '"' | "'" | '`' | null = null
420+
let inString: '"' | "'" | '`' | undefined = undefined
421421
while (i < source.length && depth > 0) {
422422
const ch = source[i]!
423423
if (inString) {
@@ -426,7 +426,7 @@ const extractPathCalls = (
426426
continue
427427
}
428428
if (ch === inString) {
429-
inString = null
429+
inString = undefined
430430
}
431431
} else {
432432
if (ch === '"' || ch === "'" || ch === '`') {

scripts/fix.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { spawn } from '@socketsecurity/lib/spawn'
2828
const WIN32 = process.platform === 'win32'
2929
const logger = getDefaultLogger()
3030

31-
async function run(
31+
export async function run(
3232
cmd: string,
3333
args: string[],
3434
{ label, required = true }: { label?: string; required?: boolean } = {},

0 commit comments

Comments
 (0)