Skip to content

Commit 858d330

Browse files
cmdcolinclaude
andcommitted
re-enable typescript-eslint rules and fix violations
Re-enable no-explicit-any (error), no-this-alias, no-non-null-assertion, and the no-unsafe-* rule cluster by fixing their root causes: - Add types:["node"] to tsconfig.lint.json so @types/node resolves - Fix test imports to use explicit .ts extensions and node: prefix (required by moduleResolution: nodenext) - Replace ~30 non-null assertions with ?? '' / conditional guards - Cast JSON.parse result to Record<string,unknown> at use site Also simplify parseMetaString (for-of over parts array, .map for trim), parseBreakend (for-of over tokens array), and parse.ts (direct match index access instead of match.slice(1,3)). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2912183 commit 858d330

13 files changed

+66
-73
lines changed

eslint.config.mjs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default defineConfig(
3434
{
3535
rules: {
3636
curly: 'error',
37+
eqeqeq: 'error',
3738
'@typescript-eslint/consistent-type-imports': 'error',
3839
'no-console': [
3940
'warn',
@@ -61,14 +62,7 @@ export default defineConfig(
6162
'error',
6263
{ 'ts-expect-error': 'allow-with-description', 'ts-ignore': true },
6364
],
64-
'@typescript-eslint/no-this-alias': 'off',
65-
'@typescript-eslint/no-unsafe-member-access': 'off',
66-
'@typescript-eslint/no-unsafe-argument': 'off',
67-
'@typescript-eslint/no-explicit-any': 'warn',
68-
'@typescript-eslint/no-unsafe-assignment': 'off',
69-
'@typescript-eslint/no-unsafe-call': 'off',
70-
'@typescript-eslint/no-unsafe-return': 'off',
71-
'@typescript-eslint/no-non-null-assertion': 'off',
65+
'@typescript-eslint/no-explicit-any': 'error',
7266
'@typescript-eslint/restrict-template-expressions': 'off',
7367
'@typescript-eslint/prefer-for-of': 'off',
7468

src/Variant.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class Variant {
4646
const fields = line.slice(0, splitPos).split('\t')
4747
const rest = line.slice(splitPos + 1)
4848
const [CHROM, POS, ID, REF, ALT, QUAL, FILTER] = fields
49-
const filter = FILTER === '.' ? undefined : FILTER!.split(';')
49+
const filter = !FILTER || FILTER === '.' ? undefined : FILTER.split(';')
5050

5151
if (strict && !fields[7]) {
5252
throw new Error(
@@ -55,11 +55,11 @@ export class Variant {
5555
}
5656

5757
this.CHROM = CHROM
58-
this.POS = +POS!
59-
this.ID = ID === '.' ? undefined : ID!.split(';')
58+
this.POS = +(POS ?? '')
59+
this.ID = !ID || ID === '.' ? undefined : ID.split(';')
6060
this.REF = REF
61-
this.ALT = ALT === '.' ? undefined : ALT!.split(',')
62-
this.QUAL = QUAL === '.' ? undefined : +QUAL!
61+
this.ALT = !ALT || ALT === '.' ? undefined : ALT.split(',')
62+
this.QUAL = !QUAL || QUAL === '.' ? undefined : +QUAL
6363
this.FILTER = filter?.length === 1 && filter[0] === 'PASS' ? 'PASS' : filter
6464
this.INFO =
6565
fields[7] === undefined || fields[7] === '.'
@@ -82,7 +82,7 @@ export class Variant {
8282
const pairsLen = infoPairs.length
8383

8484
for (let i = 0; i < pairsLen; i++) {
85-
const pair = infoPairs[i]!
85+
const pair = infoPairs[i] ?? ''
8686
const eqIdx = pair.indexOf('=')
8787
const key = eqIdx === -1 ? pair : pair.slice(0, eqIdx)
8888
const val = eqIdx === -1 ? undefined : pair.slice(eqIdx + 1)
@@ -100,7 +100,7 @@ export class Variant {
100100
if (hasDecode) {
101101
const items: (string | number | undefined)[] = []
102102
for (let j = 0; j < itemsLen; j++) {
103-
const v = rawItems[j]!
103+
const v = rawItems[j] ?? ''
104104
if (v === '.') {
105105
items.push(undefined)
106106
} else {
@@ -112,7 +112,7 @@ export class Variant {
112112
} else {
113113
const items: (string | number | undefined)[] = []
114114
for (let j = 0; j < itemsLen; j++) {
115-
const v = rawItems[j]!
115+
const v = rawItems[j] ?? ''
116116
if (v === '.') {
117117
items.push(undefined)
118118
} else {
@@ -137,18 +137,18 @@ export class Variant {
137137
const formatKeys = format.split(':')
138138
const isNumberType: boolean[] = []
139139
for (let i = 0; i < formatKeys.length; i++) {
140-
const r = this.formatMeta[formatKeys[i]!]?.Type
140+
const r = this.formatMeta[formatKeys[i] ?? '']?.Type
141141
isNumberType.push(r === 'Integer' || r === 'Float')
142142
}
143143
const numKeys = formatKeys.length
144144
const samplesLen = this.sampleNames.length
145145
for (let i = 0; i < samplesLen; i++) {
146-
const sample = this.sampleNames[i]!
146+
const sample = this.sampleNames[i] ?? ''
147147
const sampleData: Record<
148148
string,
149149
(string | number | undefined)[] | undefined
150150
> = {}
151-
const sampleStr = rest[i]!
151+
const sampleStr = rest[i] ?? ''
152152
const sampleStrLen = sampleStr.length
153153
let colStart = 0
154154
let colIdx = 0
@@ -157,22 +157,22 @@ export class Variant {
157157
if (j === sampleStrLen || sampleStr[j] === ':') {
158158
const val = sampleStr.slice(colStart, j)
159159
if (val === '' || val === '.') {
160-
sampleData[formatKeys[colIdx]!] = undefined
160+
sampleData[formatKeys[colIdx] ?? ''] = undefined
161161
} else {
162162
const items = val.split(',')
163163
const result: (string | number | undefined)[] = []
164164
if (isNumberType[colIdx]) {
165165
for (let k = 0; k < items.length; k++) {
166-
const ent = items[k]!
166+
const ent = items[k] ?? ''
167167
result.push(ent === '.' ? undefined : +ent)
168168
}
169169
} else {
170170
for (let k = 0; k < items.length; k++) {
171-
const ent = items[k]!
171+
const ent = items[k] ?? ''
172172
result.push(ent === '.' ? undefined : ent)
173173
}
174174
}
175-
sampleData[formatKeys[colIdx]!] = result
175+
sampleData[formatKeys[colIdx] ?? ''] = result
176176
}
177177
colStart = j + 1
178178
colIdx += 1

src/parse.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default class VCFParser {
3939

4040
let lastLine: string | undefined
4141
for (let i = 0; i < headerLines.length; i++) {
42-
const line = headerLines[i]!
42+
const line = headerLines[i] ?? ''
4343
if (!line.startsWith('#')) {
4444
throw new Error(`Bad line in header:\n${line}`)
4545
} else if (line.startsWith('##')) {
@@ -87,9 +87,8 @@ export default class VCFParser {
8787
if (!match) {
8888
throw new Error(`Line is not a valid metadata line: ${line}`)
8989
}
90-
const [metaKey, metaVal] = match.slice(1, 3)
91-
92-
const r = metaKey!
90+
const r = match[1] ?? ''
91+
const metaVal = match[2]
9392
if (metaVal?.startsWith('<')) {
9493
if (!(r in this.metadata)) {
9594
this.metadata[r] = {}
@@ -147,7 +146,9 @@ export default class VCFParser {
147146
if (typeof filteredMetadata !== 'object' || filteredMetadata === null) {
148147
return undefined
149148
}
150-
filteredMetadata = (filteredMetadata as Record<string, unknown>)[args[i]!]
149+
filteredMetadata = (filteredMetadata as Record<string, unknown>)[
150+
args[i] ?? ''
151+
]
151152
if (!filteredMetadata) {
152153
return filteredMetadata
153154
}

src/parseBreakend.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ export function parseBreakend(breakendString: string): Breakend | undefined {
2424
let Join
2525
let Replacement
2626
let MatePosition
27-
const tokensLen = tokens.length
28-
for (let i = 0; i < tokensLen; i++) {
29-
const tok = tokens[i]!
27+
for (const tok of tokens) {
3028
if (tok) {
3129
if (tok.includes(':')) {
3230
MatePosition = tok
@@ -69,7 +67,7 @@ export function parseBreakend(breakendString: string): Breakend | undefined {
6967
Join: 'left',
7068
Replacement,
7169
MateDirection: 'right',
72-
MatePosition: `<${res[1]!}>:1`,
70+
MatePosition: `<${res[1] ?? ''}>:1`,
7371
}
7472
: undefined
7573
}
@@ -85,7 +83,7 @@ export function parseBreakend(breakendString: string): Breakend | undefined {
8583
Join: 'right',
8684
Replacement,
8785
MateDirection: 'right',
88-
MatePosition: `<${res[2]!}>:1`,
86+
MatePosition: `<${res[2] ?? ''}>:1`,
8987
}
9088
: undefined
9189
}

src/parseGenotypesOnly.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function parseGenotypesOnly(
2121
while (pos < prerestLen && prerest.charCodeAt(pos) !== TAB) {
2222
pos++
2323
}
24-
genotypes[samples[idx]!] = prerest.slice(start, pos)
24+
genotypes[samples[idx] ?? ''] = prerest.slice(start, pos)
2525
pos++
2626
}
2727
return genotypes
@@ -44,7 +44,7 @@ export function parseGenotypesOnly(
4444
) {
4545
pos++
4646
}
47-
genotypes[samples[idx]!] = prerest.slice(start, pos)
47+
genotypes[samples[idx] ?? ''] = prerest.slice(start, pos)
4848
while (pos < prerestLen && prerest.charCodeAt(pos) !== TAB) {
4949
pos++
5050
}
@@ -72,7 +72,7 @@ export function parseGenotypesOnly(
7272
for (let j = sampleStart; j <= tabIdx; j++) {
7373
if (j === tabIdx || prerest.charCodeAt(j) === COLON) {
7474
if (colons === colonCount) {
75-
genotypes[samples[idx]!] = prerest.slice(fieldStart, j)
75+
genotypes[samples[idx] ?? ''] = prerest.slice(fieldStart, j)
7676
break
7777
}
7878
colons++

src/parseMetaString.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
// constructed with the assistance of claude AI
2-
//
3-
// I first prompted it with a regex that splits a comma separated string with
4-
// awareness of quotation from this stackoverflow question
5-
// https://stackoverflow.com/a/18893443/2129219, and asked it to add support
6-
// for square brackets
7-
//
8-
// it undid the regex into serial logic and the result was this function
91
function customSplit(str: string) {
102
const result = []
113
const chars = []
@@ -14,7 +6,7 @@ function customSplit(str: string) {
146
const strLen = str.length
157

168
for (let i = 0; i < strLen; i++) {
17-
const char = str[i]!
9+
const char = str[i] ?? ''
1810
if (char === '"') {
1911
inQuotes = !inQuotes
2012
chars.push(char)
@@ -48,15 +40,16 @@ export function parseMetaString(metaString: string) {
4840
const inside = metaString.slice(1, -1)
4941
const parts = customSplit(inside)
5042
const entries: [string, string | string[]][] = []
51-
for (let i = 0; i < parts.length; i++) {
52-
const f = parts[i]!
43+
for (const f of parts) {
5344
const [key, val] = splitFirst(f, '=')
5445
if (val && val.startsWith('[') && val.endsWith(']')) {
55-
const items = val.slice(1, -1).split(',')
56-
for (let j = 0; j < items.length; j++) {
57-
items[j] = items[j]!.trim()
58-
}
59-
entries.push([key, items])
46+
entries.push([
47+
key,
48+
val
49+
.slice(1, -1)
50+
.split(',')
51+
.map(s => s.trim()),
52+
])
6053
} else if (val && val.startsWith('"') && val.endsWith('"')) {
6154
entries.push([key, val.slice(1, -1)])
6255
} else {

test/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { parseBreakend } from '../src'
3+
import { parseBreakend } from '../src/index.ts'
44

5-
import type { Breakend } from '../src'
5+
import type { Breakend } from '../src/index.ts'
66

77
describe('testBreakend', () => {
88
it('can parse breakends', () => {

test/parse.test.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import fs from 'fs'
1+
import { readFileSync } from 'node:fs'
22

33
import { expect, test } from 'vitest'
44

5-
import VCF, { Variant, parseBreakend } from '../src'
5+
import VCF, { Variant, parseBreakend } from '../src/index.ts'
66

77
const readVcf = (file: string) => {
8-
const f = fs.readFileSync(file, 'utf8')
8+
const f = readFileSync(file, 'utf8')
99
const lines = f.split('\n')
1010
const header = [] as string[]
1111
const rest = [] as string[]
@@ -123,7 +123,7 @@ test('sniffles vcf', () => {
123123
const VCFParser = new VCF({
124124
header,
125125
})
126-
const variant = VCFParser.parseLine(lines[0])
126+
const variant = VCFParser.parseLine(lines[0] ?? '')
127127
expect(variant).toMatchSnapshot()
128128
expect(variant.SAMPLES()).toMatchSnapshot()
129129
})
@@ -133,8 +133,8 @@ test('can parse a line from the VCF spec Y chrom (haploid))', () => {
133133
const VCFParser = new VCF({
134134
header,
135135
})
136-
const variant = VCFParser.parseLine(lines[0])
137-
const variant2 = VCFParser.parseLine(lines[1])
136+
const variant = VCFParser.parseLine(lines[0] ?? '')
137+
const variant2 = VCFParser.parseLine(lines[1] ?? '')
138138
expect(variant).toMatchSnapshot()
139139
expect(variant.SAMPLES()).toMatchSnapshot()
140140
expect(variant2).toMatchSnapshot()
@@ -176,7 +176,7 @@ test('test no info strict', () => {
176176
header,
177177
strict: true,
178178
})
179-
expect(() => VCFParser.parseLine(lines[0])).toThrow(/INFO/)
179+
expect(() => VCFParser.parseLine(lines[0] ?? '')).toThrow(/INFO/)
180180
})
181181

182182
test('test no info non-strict', () => {
@@ -185,8 +185,8 @@ test('test no info non-strict', () => {
185185
header,
186186
strict: false,
187187
})
188-
expect(VCFParser.parseLine(lines[0])).toBeTruthy()
189-
expect(VCFParser.parseLine(lines[0]).GENOTYPES()).toEqual({})
188+
expect(VCFParser.parseLine(lines[0] ?? '')).toBeTruthy()
189+
expect(VCFParser.parseLine(lines[0] ?? '').GENOTYPES()).toEqual({})
190190
})
191191

192192
test('empty header lines', () => {
@@ -205,7 +205,7 @@ test('shortcut parsing with vcf 4.3 bnd example', () => {
205205

206206
const VCFParser = new VCF({ header })
207207
const variants = lines.map(line => VCFParser.parseLine(line))
208-
expect(variants.map(m => m.ALT?.[0].toString())).toEqual(
208+
expect(variants.map(m => m.ALT?.[0]?.toString())).toEqual(
209209
lines.map(line => line.split('\t')[4]),
210210
)
211211

@@ -250,8 +250,9 @@ test('sample to genotype information', () => {
250250
const VCFParser = new VCF({
251251
header,
252252
})
253-
expect(VCFParser.getMetadata().META).toMatchSnapshot()
254-
expect(VCFParser.getMetadata().SAMPLES).toMatchSnapshot()
253+
const metadata = VCFParser.getMetadata() as Record<string, unknown>
254+
expect(metadata.META).toMatchSnapshot()
255+
expect(metadata.SAMPLES).toMatchSnapshot()
255256
})
256257

257258
test('pedigree', () => {
@@ -311,7 +312,10 @@ test('Variant serializes without methods', () => {
311312
const variant = VCFParser.parseLine(
312313
'20\t14370\trs6054257\tG\tA\t29\tPASS\tNS=3;DP=14;AF=0.5;DB;H2\tGT:GQ:DP:HQ\t0|0:48:1:51,51\t1|0:48:8:51,51\t1/1:43:5:.,.',
313314
)
314-
const serialized = JSON.parse(JSON.stringify(variant))
315+
const serialized = JSON.parse(JSON.stringify(variant)) as Record<
316+
string,
317+
unknown
318+
>
315319

316320
expect(serialized.SAMPLES).toBeUndefined()
317321
expect(serialized.GENOTYPES).toBeUndefined()

test/parseGenotypesOnly-edge-cases.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'vitest'
22

3-
import { parseGenotypesOnly } from '../src/parseGenotypesOnly'
3+
import { parseGenotypesOnly } from '../src/parseGenotypesOnly.ts'
44

55
test('last sample with 3-char GT and no trailing tab', () => {
66
// Single sample, no trailing tab
@@ -120,7 +120,7 @@ test('haploid genotypes - many samples', () => {
120120

121121
const expected = {} as Record<string, string>
122122
samples.forEach((s, i) => {
123-
expected[s] = gts[i]!
123+
expected[s] = gts[i] ?? ''
124124
})
125125

126126
expect(result).toEqual(expected)
@@ -368,7 +368,7 @@ test('large scale mixed ploidy', () => {
368368

369369
const expected = {} as Record<string, string>
370370
samples.forEach((s, i) => {
371-
expected[s] = gts[i]!
371+
expected[s] = gts[i] ?? ''
372372
})
373373

374374
expect(result).toEqual(expected)

test/parseGenotypesOnly-ultrafast-edge.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'vitest'
22

3-
import { parseGenotypesOnly } from '../src/parseGenotypesOnly'
3+
import { parseGenotypesOnly } from '../src/parseGenotypesOnly.ts'
44

55
test('ultra-fast path should not be tricked by mixed ploidy with matching length', () => {
66
// 4 samples with total length = 15 = 4*4-1

0 commit comments

Comments
 (0)