diff --git a/src/components/Cell.tsx b/src/components/Cell.tsx index c6271b0b..ecfaa479 100644 --- a/src/components/Cell.tsx +++ b/src/components/Cell.tsx @@ -38,12 +38,20 @@ export default function CellView({ source, row, col, config }: CellProps) { setProgress(0.75) const df = parquetDataFrame(from, metadata) const asyncRows = df.rows({ start: row, end: row + 1 }) - if (asyncRows.length !== 1) { + if (asyncRows.length > 1 || !(0 in asyncRows)) { throw new Error(`Expected 1 row, got ${asyncRows.length}`) } const asyncRow = asyncRows[0] // Await cell data - const text = await asyncRow.cells[df.header[col]].then(stringify) + const columnName = df.header[col] + if (columnName === undefined) { + throw new Error(`Column name missing at index col=${col}`) + } + const asyncCell = asyncRow.cells[columnName] + if (asyncCell === undefined) { + throw new Error(`Cell missing at column ${columnName}`) + } + const text = await asyncCell.then(stringify) setText(text) setError(undefined) } catch (error) { diff --git a/src/components/Folder.tsx b/src/components/Folder.tsx index d783b29c..138fbe45 100644 --- a/src/components/Folder.tsx +++ b/src/components/Folder.tsx @@ -47,7 +47,7 @@ export default function Folder({ source, config }: FolderProps) { setSearchQuery('') } else if (e.key === 'Enter') { // if there is only one result, view it - if (filtered?.length === 1) { + if (filtered?.length === 1 && 0 in filtered) { const key = join(source.prefix, filtered[0].name) if (key.endsWith('/')) { // clear search because we're about to change folder diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 94dec57d..1d18976a 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -27,7 +27,7 @@ function parseMarkdown(text: string): Token[] { const line = lines[i] // Skip blank lines - if (line.trim() === '') { + if (line === undefined || line.trim() === '') { i++ continue } @@ -36,9 +36,13 @@ function parseMarkdown(text: string): Token[] { if (line.startsWith('```')) { const language = line.slice(3).trim() || undefined i++ - const codeLines = [] - while (i < lines.length && !lines[i].startsWith('```')) { - codeLines.push(lines[i]) + const codeLines: string[] = [] + while (i < lines.length && !lines[i]?.startsWith('```')) { + const currentLine = lines[i] + if (currentLine === undefined) { + throw new Error(`Line is undefined at index ${i}.`) + } + codeLines.push(currentLine) i++ } i++ // skip the closing ``` @@ -48,7 +52,10 @@ function parseMarkdown(text: string): Token[] { // Heading const headingMatch = /^(#{1,6})\s+(.*)/.exec(line) - if (headingMatch) { + if (headingMatch !== null) { + if (!(1 in headingMatch) || !(2 in headingMatch)) { + throw new Error('Missing entries in regex matches') + } const level = headingMatch[1].length tokens.push({ type: 'heading', @@ -61,7 +68,10 @@ function parseMarkdown(text: string): Token[] { // List (ordered or unordered) const listMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line) - if (listMatch) { + if (listMatch !== null) { + if (!(1 in listMatch) || !(2 in listMatch)) { + throw new Error('Missing entries in regex matches') + } const baseIndent = listMatch[1].length const ordered = /^\d+\./.test(listMatch[2]) const [items, newIndex] = parseList(lines, i, baseIndent) @@ -72,9 +82,13 @@ function parseMarkdown(text: string): Token[] { // Blockquote if (line.startsWith('>')) { - const quoteLines = [] - while (i < lines.length && lines[i].startsWith('>')) { - quoteLines.push(lines[i].replace(/^>\s?/, '')) + const quoteLines: string[] = [] + while (i < lines.length && lines[i]?.startsWith('>')) { + const line = lines[i] + if (line === undefined) { + throw new Error(`Index ${i} not found in lines`) + } + quoteLines.push(line.replace(/^>\s?/, '')) i++ } tokens.push({ @@ -85,9 +99,13 @@ function parseMarkdown(text: string): Token[] { } // Paragraph - const paraLines = [] - while (i < lines.length && lines[i].trim() !== '') { - paraLines.push(lines[i]) + const paraLines: string[] = [] + while (i < lines.length && lines[i]?.trim() !== '') { + const line = lines[i] + if (line === undefined) { + throw new Error(`Index ${i} not found in lines`) + } + paraLines.push(line) i++ } tokens.push({ @@ -104,16 +122,18 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[] let i = start while (i < lines.length) { + const line = lines[i] + // End of list if blank line or no more lines - if (lines[i].trim() === '') { + if (line === undefined || line.trim() === '') { i++ continue } // This matches a new top-level bullet/number for the list - const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(lines[i]) + const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line) // If we don't find a bullet/number at the same indent, break out - if (!match || match[1].length !== baseIndent) { + if (match === null || !(1 in match) || match[1].length !== baseIndent || !(3 in match)) { break } @@ -130,7 +150,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[] // Now parse subsequent indented lines as sub-items or sub-blocks while (i < lines.length) { const subline = lines[i] - if (subline.trim() === '') { + if (subline === undefined || subline.trim() === '') { i++ continue } @@ -141,9 +161,13 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[] // If it’s a fenced code block, parse until closing fence const language = trimmed.slice(3).trim() || undefined i++ - const codeLines = [] - while (i < lines.length && !lines[i].trimStart().startsWith('```')) { - codeLines.push(lines[i]) + const codeLines: string[] = [] + while (i < lines.length && !lines[i]?.trimStart().startsWith('```')) { + const line = lines[i] + if (line === undefined) { + throw new Error(`Line is undefined at index ${i}`) + } + codeLines.push(line) i++ } i++ // skip the closing ``` @@ -157,7 +181,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[] // Check for nested list const sublistMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(subline) - if (sublistMatch && sublistMatch[1].length > baseIndent) { + if (sublistMatch && 1 in sublistMatch && sublistMatch[1].length > baseIndent && 2 in sublistMatch) { const newBaseIndent = sublistMatch[1].length const ordered = /^\d+\./.test(sublistMatch[2]) const [subItems, newIndex] = parseList(lines, i, newBaseIndent) @@ -253,7 +277,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] { const [linkTextTokens, consumed] = parseInlineRecursive(text.slice(i), ']') i += consumed if (i >= text.length || text[i] !== ']') { - tokens.push({ type: 'text', content: text[start] }) + const startText = text[start] + if (startText === undefined) { + throw new Error(`Text is undefined at index ${start}`) + } + tokens.push({ type: 'text', content: startText }) continue } i++ // skip ']' @@ -285,7 +313,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] { i++ let code = '' while (i < text.length && text[i] !== '`') { - code += text[i] + const character = text[i] + if (character === undefined) { + throw new Error(`Character is undefined at index ${i}`) + } + code += character i++ } i++ @@ -311,10 +343,18 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] { if (delimiter === '*') { let j = i - 1 while (j >= 0 && text[j] === ' ') j-- - const prevIsDigit = j >= 0 && /\d/.test(text[j]) + const characterAtJ = text[j] + if (characterAtJ === undefined) { + throw new Error(`Character at index ${j} is undefined`) + } + const prevIsDigit = j >= 0 && /\d/.test(characterAtJ) let k = i + 1 while (k < text.length && text[k] === ' ') k++ - const nextIsDigit = k < text.length && /\d/.test(text[k]) + const characterAtK = text[k] + if (characterAtK === undefined) { + throw new Error(`Character at index ${j} is undefined`) + } + const nextIsDigit = k < text.length && /\d/.test(characterAtK) if (prevIsDigit && nextIsDigit) { tokens.push({ type: 'text', content: delimiter }) i++ diff --git a/src/components/viewers/CellPanel.tsx b/src/components/viewers/CellPanel.tsx index aa6dc7bd..dc2dbcee 100644 --- a/src/components/viewers/CellPanel.tsx +++ b/src/components/viewers/CellPanel.tsx @@ -23,12 +23,21 @@ export default function CellPanel({ df, row, col, setProgress, setError, onClose try { setProgress(0.5) const asyncRows = df.rows({ start: row, end: row + 1 }) - if (asyncRows.length !== 1) { + if (asyncRows.length > 1 || !(0 in asyncRows)) { throw new Error(`Expected 1 row, got ${asyncRows.length}`) } const asyncRow = asyncRows[0] // Await cell data - const text = await asyncRow.cells[df.header[col]].then(stringify) + const columnName = df.header[col] + if (columnName === undefined) { + throw new Error(`Column name missing at index col=${col}`) + } + const asyncCell = asyncRow.cells[columnName] + if (asyncCell === undefined) { + throw new Error(`Cell missing at column ${columnName}`) + } + /* TODO(SL): use the same implementation of stringify, here and in Cell.tsx */ + const text = await asyncCell.then(cell => stringify(cell as unknown) ?? '{}') setText(text) } catch (error) { setError(error as Error) diff --git a/src/components/viewers/ImageView.tsx b/src/components/viewers/ImageView.tsx index db7a17aa..734bcf66 100644 --- a/src/components/viewers/ImageView.tsx +++ b/src/components/viewers/ImageView.tsx @@ -70,13 +70,13 @@ export default function ImageView({ source, setError }: ViewerProps) { function arrayBufferToBase64(buffer: ArrayBuffer): string { let binary = '' const bytes = new Uint8Array(buffer) - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]) + for (const byte of bytes) { + binary += String.fromCharCode(byte) } return btoa(binary) } function contentType(filename: string): string { const ext = filename.split('.').pop() ?? '' - return contentTypes[ext] || 'image/png' + return contentTypes[ext] ?? 'image/png' } diff --git a/src/lib/tableProvider.ts b/src/lib/tableProvider.ts index bea4baed..cae5e423 100644 --- a/src/lib/tableProvider.ts +++ b/src/lib/tableProvider.ts @@ -17,8 +17,11 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData): function fetchRowGroup(groupIndex: number) { if (!groups[groupIndex]) { - const rowStart = groupEnds[groupIndex - 1] || 0 + const rowStart = groupEnds[groupIndex - 1] ?? 0 const rowEnd = groupEnds[groupIndex] + if (rowEnd === undefined) { + throw new Error(`Missing groupEnd for groupIndex: ${groupIndex}`) + } // Initialize with resolvable promises for (let i = rowStart; i < rowEnd; i++) { data[i] = resolvableRow(header) @@ -26,11 +29,21 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData): parquetQueryWorker({ from, metadata, rowStart, rowEnd }) .then((groupData) => { for (let i = rowStart; i < rowEnd; i++) { - data[i]?.index.resolve(i) - for (const [key, value] of Object.entries( - groupData[i - rowStart] - )) { - data[i]?.cells[key].resolve(value) + const dataRow = data[i] + if (dataRow === undefined) { + throw new Error(`Missing data row for index ${i}`) + } + dataRow.index.resolve(i) + const row = groupData[i - rowStart] + if (row === undefined) { + throw new Error(`Missing row in groupData for index: ${i - rowStart}`) + } + for (const [key, value] of Object.entries(row)) { + const cell = dataRow.cells[key] + if (cell === undefined) { + throw new Error(`Missing column in dataRow for column ${key}`) + } + cell.resolve(value) } } }) @@ -68,19 +81,31 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData): // Re-assemble data in sorted order into wrapped for (let i = start; i < end; i++) { const index = indices[i] + if (index === undefined) { + throw new Error(`index ${i} not found in indices`) + } const row = data[index] if (row === undefined) { throw new Error('Row not fetched') } const { cells } = row - wrapped[i - start].index.resolve(index) + const wrappedRow = wrapped[i - start] + if (wrappedRow === undefined) { + throw new Error(`Wrapped row missing at index ${i - start}`) + } + wrappedRow.index.resolve(index) for (const key of header) { - if (key in cells) { + const cell = cells[key] + if (cell) { // TODO(SL): should we remove this check? It makes sense only if header change // but if so, I guess we will have more issues - cells[key] + cell .then((value: unknown) => { - wrapped[i - start]?.cells[key].resolve(value) + const wrappedCell = wrappedRow.cells[key] + if (wrappedCell === undefined) { + throw new Error(`Wrapped cell not found for column ${key}`) + } + wrappedCell.resolve(value) }) .catch((error: unknown) => { console.error('Error resolving sorted row', error) @@ -98,8 +123,12 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData): return wrapped } else { for (let i = 0; i < groups.length; i++) { - const groupStart = groupEnds[i - 1] || 0 - if (start < groupEnds[i] && end > groupStart) { + const groupStart = groupEnds[i - 1] ?? 0 + const groupEnd = groupEnds[i] + if (groupEnd === undefined) { + throw new Error(`Missing group end at index ${i}`) + } + if (start < groupEnd && end > groupStart) { fetchRowGroup(i) } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3d0e9151..6940bd16 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -63,11 +63,15 @@ export function formatFileSize(bytes: number): string { if (bytes === 0) return '0 b' const i = Math.floor(Math.log2(bytes) / 10) if (i === 0) return bytes.toLocaleString('en-US') + ' b' + const size = sizes[i] + if (size === undefined) { + throw new Error(`Size not found at index ${i}`) + } const base = bytes / Math.pow(1024, i) return ( (base < 10 ? base.toFixed(1) : Math.round(base)).toLocaleString('en-US') + ' ' + - sizes[i] + size ) } diff --git a/src/lib/workers/parquetWorker.ts b/src/lib/workers/parquetWorker.ts index c6fb2870..96a2c68f 100644 --- a/src/lib/workers/parquetWorker.ts +++ b/src/lib/workers/parquetWorker.ts @@ -38,7 +38,7 @@ self.onmessage = async ({ data }: { throw new Error('sortIndex requires all rows') const sortColumn = await parquetQuery({ metadata, file, columns: [orderBy], compressors }) const indices = Array.from(sortColumn, (_, index) => index).sort((a, b) => - compare(sortColumn[a][orderBy], sortColumn[b][orderBy]) + compare(sortColumn[a]?.[orderBy], sortColumn[b]?.[orderBy]) ) postIndicesMessage({ indices, queryId }) } catch (error) { diff --git a/tsconfig.json b/tsconfig.json index 150081b5..f1f2e69d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, + "noUncheckedIndexedAccess": true, "rootDir": "src", "outDir": "lib",