Skip to content

Commit d6336d3

Browse files
committed
Add a typescript rule so that array<T>[i] is T | undefined
Because i could be out of bounds (or even: not an integer). This rule is sometimes really annoying (some call it pedantic), but I think it can help find bugs. I'm enabling it as a distinct PR, before a bigger PR where I upgrade hightable, and need to add code to sort the rows along multiple columns, where I prefer to have this rule enabled, as in hightable.
1 parent 57dc338 commit d6336d3

File tree

9 files changed

+137
-46
lines changed

9 files changed

+137
-46
lines changed

src/components/Cell.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,20 @@ export default function CellView({ source, row, col, config }: CellProps) {
3838
setProgress(0.75)
3939
const df = parquetDataFrame(from, metadata)
4040
const asyncRows = df.rows({ start: row, end: row + 1 })
41-
if (asyncRows.length !== 1) {
41+
if (asyncRows.length > 1 || !(0 in asyncRows)) {
4242
throw new Error(`Expected 1 row, got ${asyncRows.length}`)
4343
}
4444
const asyncRow = asyncRows[0]
4545
// Await cell data
46-
const text = await asyncRow.cells[df.header[col]].then(stringify)
46+
const columnName = df.header[col]
47+
if (columnName === undefined) {
48+
throw new Error(`Column name missing at index col=${col}`)
49+
}
50+
const asyncCell = asyncRow.cells[columnName]
51+
if (asyncCell === undefined) {
52+
throw new Error(`Cell missing at column ${columnName}`)
53+
}
54+
const text = await asyncCell.then(stringify)
4755
setText(text)
4856
setError(undefined)
4957
} catch (error) {

src/components/Folder.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default function Folder({ source, config }: FolderProps) {
4747
setSearchQuery('')
4848
} else if (e.key === 'Enter') {
4949
// if there is only one result, view it
50-
if (filtered?.length === 1) {
50+
if (filtered?.length === 1 && 0 in filtered) {
5151
const key = join(source.prefix, filtered[0].name)
5252
if (key.endsWith('/')) {
5353
// clear search because we're about to change folder

src/components/Markdown.tsx

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function parseMarkdown(text: string): Token[] {
2727
const line = lines[i]
2828

2929
// Skip blank lines
30-
if (line.trim() === '') {
30+
if (line === undefined || line.trim() === '') {
3131
i++
3232
continue
3333
}
@@ -36,9 +36,13 @@ function parseMarkdown(text: string): Token[] {
3636
if (line.startsWith('```')) {
3737
const language = line.slice(3).trim() || undefined
3838
i++
39-
const codeLines = []
40-
while (i < lines.length && !lines[i].startsWith('```')) {
41-
codeLines.push(lines[i])
39+
const codeLines: string[] = []
40+
while (i < lines.length && !lines[i]?.startsWith('```')) {
41+
const currentLine = lines[i]
42+
if (currentLine === undefined) {
43+
throw new Error(`Line is undefined at index ${i}.`)
44+
}
45+
codeLines.push(currentLine)
4246
i++
4347
}
4448
i++ // skip the closing ```
@@ -48,7 +52,10 @@ function parseMarkdown(text: string): Token[] {
4852

4953
// Heading
5054
const headingMatch = /^(#{1,6})\s+(.*)/.exec(line)
51-
if (headingMatch) {
55+
if (headingMatch !== null) {
56+
if (!(1 in headingMatch) || !(2 in headingMatch)) {
57+
throw new Error('Missing entries in regex matches')
58+
}
5259
const level = headingMatch[1].length
5360
tokens.push({
5461
type: 'heading',
@@ -61,7 +68,10 @@ function parseMarkdown(text: string): Token[] {
6168

6269
// List (ordered or unordered)
6370
const listMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line)
64-
if (listMatch) {
71+
if (listMatch !== null) {
72+
if (!(1 in listMatch) || !(2 in listMatch)) {
73+
throw new Error('Missing entries in regex matches')
74+
}
6575
const baseIndent = listMatch[1].length
6676
const ordered = /^\d+\./.test(listMatch[2])
6777
const [items, newIndex] = parseList(lines, i, baseIndent)
@@ -72,9 +82,13 @@ function parseMarkdown(text: string): Token[] {
7282

7383
// Blockquote
7484
if (line.startsWith('>')) {
75-
const quoteLines = []
76-
while (i < lines.length && lines[i].startsWith('>')) {
77-
quoteLines.push(lines[i].replace(/^>\s?/, ''))
85+
const quoteLines: string[] = []
86+
while (i < lines.length && lines[i]?.startsWith('>')) {
87+
const line = lines[i]
88+
if (line === undefined) {
89+
throw new Error(`Index ${i} not found in lines`)
90+
}
91+
quoteLines.push(line.replace(/^>\s?/, ''))
7892
i++
7993
}
8094
tokens.push({
@@ -85,9 +99,13 @@ function parseMarkdown(text: string): Token[] {
8599
}
86100

87101
// Paragraph
88-
const paraLines = []
89-
while (i < lines.length && lines[i].trim() !== '') {
90-
paraLines.push(lines[i])
102+
const paraLines: string[] = []
103+
while (i < lines.length && lines[i]?.trim() !== '') {
104+
const line = lines[i]
105+
if (line === undefined) {
106+
throw new Error(`Index ${i} not found in lines`)
107+
}
108+
paraLines.push(line)
91109
i++
92110
}
93111
tokens.push({
@@ -104,16 +122,18 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
104122
let i = start
105123

106124
while (i < lines.length) {
125+
const line = lines[i]
126+
107127
// End of list if blank line or no more lines
108-
if (lines[i].trim() === '') {
128+
if (line === undefined || line.trim() === '') {
109129
i++
110130
continue
111131
}
112132

113133
// This matches a new top-level bullet/number for the list
114-
const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(lines[i])
134+
const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line)
115135
// If we don't find a bullet/number at the same indent, break out
116-
if (!match || match[1].length !== baseIndent) {
136+
if (match === null || !(1 in match) || match[1].length !== baseIndent || !(3 in match)) {
117137
break
118138
}
119139

@@ -130,7 +150,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
130150
// Now parse subsequent indented lines as sub-items or sub-blocks
131151
while (i < lines.length) {
132152
const subline = lines[i]
133-
if (subline.trim() === '') {
153+
if (subline === undefined || subline.trim() === '') {
134154
i++
135155
continue
136156
}
@@ -141,9 +161,13 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
141161
// If it’s a fenced code block, parse until closing fence
142162
const language = trimmed.slice(3).trim() || undefined
143163
i++
144-
const codeLines = []
145-
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
146-
codeLines.push(lines[i])
164+
const codeLines: string[] = []
165+
while (i < lines.length && !lines[i]?.trimStart().startsWith('```')) {
166+
const line = lines[i]
167+
if (line === undefined) {
168+
throw new Error(`Line is undefined at index ${i}`)
169+
}
170+
codeLines.push(line)
147171
i++
148172
}
149173
i++ // skip the closing ```
@@ -157,7 +181,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
157181

158182
// Check for nested list
159183
const sublistMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(subline)
160-
if (sublistMatch && sublistMatch[1].length > baseIndent) {
184+
if (sublistMatch && 1 in sublistMatch && sublistMatch[1].length > baseIndent && 2 in sublistMatch) {
161185
const newBaseIndent = sublistMatch[1].length
162186
const ordered = /^\d+\./.test(sublistMatch[2])
163187
const [subItems, newIndex] = parseList(lines, i, newBaseIndent)
@@ -253,7 +277,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
253277
const [linkTextTokens, consumed] = parseInlineRecursive(text.slice(i), ']')
254278
i += consumed
255279
if (i >= text.length || text[i] !== ']') {
256-
tokens.push({ type: 'text', content: text[start] })
280+
const startText = text[start]
281+
if (startText === undefined) {
282+
throw new Error(`Text is undefined at index ${start}`)
283+
}
284+
tokens.push({ type: 'text', content: startText })
257285
continue
258286
}
259287
i++ // skip ']'
@@ -285,7 +313,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
285313
i++
286314
let code = ''
287315
while (i < text.length && text[i] !== '`') {
288-
code += text[i]
316+
const character = text[i]
317+
if (character === undefined) {
318+
throw new Error(`Character is undefined at index ${i}`)
319+
}
320+
code += character
289321
i++
290322
}
291323
i++
@@ -311,10 +343,18 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
311343
if (delimiter === '*') {
312344
let j = i - 1
313345
while (j >= 0 && text[j] === ' ') j--
314-
const prevIsDigit = j >= 0 && /\d/.test(text[j])
346+
const characterAtJ = text[j]
347+
if (characterAtJ === undefined) {
348+
throw new Error(`Character at index ${j} is undefined`)
349+
}
350+
const prevIsDigit = j >= 0 && /\d/.test(characterAtJ)
315351
let k = i + 1
316352
while (k < text.length && text[k] === ' ') k++
317-
const nextIsDigit = k < text.length && /\d/.test(text[k])
353+
const characterAtK = text[k]
354+
if (characterAtK === undefined) {
355+
throw new Error(`Character at index ${j} is undefined`)
356+
}
357+
const nextIsDigit = k < text.length && /\d/.test(characterAtK)
318358
if (prevIsDigit && nextIsDigit) {
319359
tokens.push({ type: 'text', content: delimiter })
320360
i++

src/components/viewers/CellPanel.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ export default function CellPanel({ df, row, col, setProgress, setError, onClose
2323
try {
2424
setProgress(0.5)
2525
const asyncRows = df.rows({ start: row, end: row + 1 })
26-
if (asyncRows.length !== 1) {
26+
if (asyncRows.length > 1 || !(0 in asyncRows)) {
2727
throw new Error(`Expected 1 row, got ${asyncRows.length}`)
2828
}
2929
const asyncRow = asyncRows[0]
3030
// Await cell data
31-
const text = await asyncRow.cells[df.header[col]].then(stringify)
31+
const columnName = df.header[col]
32+
if (columnName === undefined) {
33+
throw new Error(`Column name missing at index col=${col}`)
34+
}
35+
const asyncCell = asyncRow.cells[columnName]
36+
if (asyncCell === undefined) {
37+
throw new Error(`Cell missing at column ${columnName}`)
38+
}
39+
/* TODO(SL): use the same implementation of stringify, here and in Cell.tsx */
40+
const text = await asyncCell.then(cell => stringify(cell as unknown) ?? '{}')
3241
setText(text)
3342
} catch (error) {
3443
setError(error as Error)

src/components/viewers/ImageView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ export default function ImageView({ source, setError }: ViewerProps) {
7070
function arrayBufferToBase64(buffer: ArrayBuffer): string {
7171
let binary = ''
7272
const bytes = new Uint8Array(buffer)
73-
for (let i = 0; i < bytes.byteLength; i++) {
74-
binary += String.fromCharCode(bytes[i])
73+
for (const byte of bytes) {
74+
binary += String.fromCharCode(byte)
7575
}
7676
return btoa(binary)
7777
}
7878

7979
function contentType(filename: string): string {
8080
const ext = filename.split('.').pop() ?? ''
81-
return contentTypes[ext] || 'image/png'
81+
return contentTypes[ext] ?? 'image/png'
8282
}

src/lib/tableProvider.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,33 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):
1717

1818
function fetchRowGroup(groupIndex: number) {
1919
if (!groups[groupIndex]) {
20-
const rowStart = groupEnds[groupIndex - 1] || 0
20+
const rowStart = groupEnds[groupIndex - 1] ?? 0
2121
const rowEnd = groupEnds[groupIndex]
22+
if (rowEnd === undefined) {
23+
throw new Error(`Missing groupEnd for groupIndex: ${groupIndex}`)
24+
}
2225
// Initialize with resolvable promises
2326
for (let i = rowStart; i < rowEnd; i++) {
2427
data[i] = resolvableRow(header)
2528
}
2629
parquetQueryWorker({ from, metadata, rowStart, rowEnd })
2730
.then((groupData) => {
2831
for (let i = rowStart; i < rowEnd; i++) {
29-
data[i]?.index.resolve(i)
30-
for (const [key, value] of Object.entries(
31-
groupData[i - rowStart]
32-
)) {
33-
data[i]?.cells[key].resolve(value)
32+
const dataRow = data[i]
33+
if (dataRow === undefined) {
34+
throw new Error(`Missing data row for index ${i}`)
35+
}
36+
dataRow.index.resolve(i)
37+
const row = groupData[i - rowStart]
38+
if (row === undefined) {
39+
throw new Error(`Missing row in groupData for index: ${i - rowStart}`)
40+
}
41+
for (const [key, value] of Object.entries(row)) {
42+
const cell = dataRow.cells[key]
43+
if (cell === undefined) {
44+
throw new Error(`Missing column in dataRow for column ${key}`)
45+
}
46+
cell.resolve(value)
3447
}
3548
}
3649
})
@@ -68,19 +81,31 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):
6881
// Re-assemble data in sorted order into wrapped
6982
for (let i = start; i < end; i++) {
7083
const index = indices[i]
84+
if (index === undefined) {
85+
throw new Error(`index ${i} not found in indices`)
86+
}
7187
const row = data[index]
7288
if (row === undefined) {
7389
throw new Error('Row not fetched')
7490
}
7591
const { cells } = row
76-
wrapped[i - start].index.resolve(index)
92+
const wrappedRow = wrapped[i - start]
93+
if (wrappedRow === undefined) {
94+
throw new Error(`Wrapped row missing at index ${i - start}`)
95+
}
96+
wrappedRow.index.resolve(index)
7797
for (const key of header) {
78-
if (key in cells) {
98+
const cell = cells[key]
99+
if (cell) {
79100
// TODO(SL): should we remove this check? It makes sense only if header change
80101
// but if so, I guess we will have more issues
81-
cells[key]
102+
cell
82103
.then((value: unknown) => {
83-
wrapped[i - start]?.cells[key].resolve(value)
104+
const wrappedCell = wrappedRow.cells[key]
105+
if (wrappedCell === undefined) {
106+
throw new Error(`Wrapped cell not found for column ${key}`)
107+
}
108+
wrappedCell.resolve(value)
84109
})
85110
.catch((error: unknown) => {
86111
console.error('Error resolving sorted row', error)
@@ -98,8 +123,12 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):
98123
return wrapped
99124
} else {
100125
for (let i = 0; i < groups.length; i++) {
101-
const groupStart = groupEnds[i - 1] || 0
102-
if (start < groupEnds[i] && end > groupStart) {
126+
const groupStart = groupEnds[i - 1] ?? 0
127+
const groupEnd = groupEnds[i]
128+
if (groupEnd === undefined) {
129+
throw new Error(`Missing group end at index ${i}`)
130+
}
131+
if (start < groupEnd && end > groupStart) {
103132
fetchRowGroup(i)
104133
}
105134
}

src/lib/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,15 @@ export function formatFileSize(bytes: number): string {
6363
if (bytes === 0) return '0 b'
6464
const i = Math.floor(Math.log2(bytes) / 10)
6565
if (i === 0) return bytes.toLocaleString('en-US') + ' b'
66+
const size = sizes[i]
67+
if (size === undefined) {
68+
throw new Error(`Size not found at index ${i}`)
69+
}
6670
const base = bytes / Math.pow(1024, i)
6771
return (
6872
(base < 10 ? base.toFixed(1) : Math.round(base)).toLocaleString('en-US') +
6973
' ' +
70-
sizes[i]
74+
size
7175
)
7276
}
7377

src/lib/workers/parquetWorker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ self.onmessage = async ({ data }: {
3838
throw new Error('sortIndex requires all rows')
3939
const sortColumn = await parquetQuery({ metadata, file, columns: [orderBy], compressors })
4040
const indices = Array.from(sortColumn, (_, index) => index).sort((a, b) =>
41-
compare<unknown>(sortColumn[a][orderBy], sortColumn[b][orderBy])
41+
compare<unknown>(sortColumn[a]?.[orderBy], sortColumn[b]?.[orderBy])
4242
)
4343
postIndicesMessage({ indices, queryId })
4444
} catch (error) {

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"noUnusedParameters": true,
1717
"noFallthroughCasesInSwitch": true,
1818
"noUncheckedSideEffectImports": true,
19+
"noUncheckedIndexedAccess": true,
1920

2021
"rootDir": "src",
2122
"outDir": "lib",

0 commit comments

Comments
 (0)