diff --git a/indexed-access-baseline.json b/indexed-access-baseline.json index c84acffd..09f0bf9b 100644 --- a/indexed-access-baseline.json +++ b/indexed-access-baseline.json @@ -1,7 +1,15 @@ { - "total": 85, + "total": 84, "files": { - "projects/pathway-browser/src/app/render/render.component.ts": 62, - "projects/pathway-browser/src/app/services/diagram.service.ts": 23 + "projects/reactome-gsa-form/src/lib/dataset-form/datasets/select-dataset/dataset-types/local-data/local-data.component.ts": 4, + "projects/reactome-gsa-form/src/lib/state/dataset-source/dataset-source.effect.ts": 2, + "projects/reactome-gsa-form/src/lib/state/dataset/dataset.effect.ts": 1, + "projects/reactome-gsa-form/src/lib/state/dataset/dataset.reducer.ts": 3, + "projects/reactome-gsa-form/src/lib/state/dataset/dataset.selector.ts": 4, + "projects/reactome-gsa-form/src/lib/state/method/method.effect.ts": 2, + "projects/reactome-gsa-form/src/lib/state/method/method.reducer.ts": 1, + "projects/reactome-gsa-form/src/lib/utilities/species/species.pipe.ts": 1, + "projects/reactome-table/src/lib/state/table.store.ts": 63, + "projects/reactome-table/src/lib/utils/table.util.ts": 3 } } diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.html b/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.html index 70f57db0..ef263c3d 100644 --- a/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.html +++ b/projects/pathway-browser/src/app/details/tabs/result-tab/found-table/found-table.component.html @@ -34,7 +34,7 @@ @for (resource of resources(); track resource) { - @let resourceColumn = resourceColumnIds()[$index]; + @let resourceColumn = resourceColumnIds()[$index] ?? 'entities-' + resource; @let prefix = resourceToIdentifiersPrefix.get(resource)!; {{ resource | titlecase }} entities @@ -64,12 +64,15 @@
- + @let value = found.exp[i]; + @if (value !== undefined) { + + }
> diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.html b/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.html index 7704d401..dbd513a0 100644 --- a/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.html +++ b/projects/pathway-browser/src/app/details/tabs/result-tab/not-found-table/not-found-table.component.html @@ -19,12 +19,15 @@ {{ expressionColumnNames()[i] }} - + @let value = notFound.exp[i]; + @if (value !== undefined) { + + } > } diff --git a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.html b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.html index c3c79ee7..8d94ca25 100644 --- a/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.html +++ b/projects/pathway-browser/src/app/details/tabs/result-tab/result-tab.component.html @@ -55,8 +55,9 @@

@for (sample of analysis.samples(); track sample; let i = $index) { - @if (selected.values[i] !== undefined) { - + @let value = selected.values[i]; + @if (value !== undefined) { + } } @@ -209,12 +210,15 @@

{{ expressionColumnNames()[i] }} - + @let value = pathway.entities.exp[i]; + @if (value !== undefined) { + + } > } diff --git a/projects/pathway-browser/src/app/render/render.component.ts b/projects/pathway-browser/src/app/render/render.component.ts index 87d59527..98619293 100644 --- a/projects/pathway-browser/src/app/render/render.component.ts +++ b/projects/pathway-browser/src/app/render/render.component.ts @@ -417,11 +417,12 @@ export class RenderComponent { */ private async frameCanvas(scale: number): Promise { const { diagram, instances } = this.exportableInstances(); - if (diagram && instances.length) { + const [instance] = instances; + if (diagram && instance) { // White, not transparent. Every animated format in play here either has no // alpha channel or only a single transparent index, so a transparent // background composites to black rather than to nothing. - return diagram.exportCanvas(instances[0], { full: true, scale, bg: '#ffffff' }); + return diagram.exportCanvas(instance, { full: true, scale, bg: '#ffffff' }); } // An illustration is inline SVG rather than a canvas, so a frame has to be @@ -779,6 +780,8 @@ export class RenderComponent { const a = points[from]; const b = points[to]; + // In range: both are indexes of points, from the stack. + if (!a || !b) continue; const dx = b.x - a.x; const dy = b.y - a.y; const span = Math.hypot(dx, dy); @@ -787,6 +790,7 @@ export class RenderComponent { let at = -1; for (let index = from + 1; index < to; index++) { const point = points[index]; + if (!point) continue; // Distance to the chord, or to its endpoint when the chord is a point. const distance = span === 0 @@ -840,7 +844,7 @@ export class RenderComponent { private transformScale(element: Element): number { const transform = element.getAttribute('transform') ?? element.getAttribute('style') ?? ''; const scale = /scale\(\s*([\d.]+)/.exec(transform); - const value = scale ? parseFloat(scale[1]) : 1; + const value = parseFloat(scale?.[1] ?? '1'); return Number.isFinite(value) && value > 0 ? value : 1; } @@ -1053,18 +1057,19 @@ export class RenderComponent { const rgb = /^rgba?\(([^)]+)\)$/.exec(text); if (rgb) { - const parts = rgb[1].split(/[,\s/]+/).map((part) => parseFloat(part)); + const parts = (rgb[1] ?? '').split(/[,\s/]+/).map((part) => parseFloat(part)); if (parts.length >= 3 && parts.slice(0, 3).every((part) => Number.isFinite(part))) { const own = parts.length > 3 && Number.isFinite(parts[3]) ? parts[3] : 1; return withAlpha(parts.slice(0, 3), own); } } const hex = /^#?([0-9a-f]{6})$/i.exec(text); - if (hex) { - return withAlpha([0, 2, 4].map((at) => parseInt(hex[1].slice(at, at + 2), 16))); + if (hex?.[1]) { + const digits = hex[1]; + return withAlpha([0, 2, 4].map((at) => parseInt(digits.slice(at, at + 2), 16))); } const short = /^#?([0-9a-f]{3})$/i.exec(text); - if (short) { + if (short?.[1]) { return withAlpha([...short[1]].map((digit) => parseInt(digit + digit, 16))); } return null; @@ -1153,6 +1158,8 @@ export class RenderComponent { for (let index = 0; index < polygon.length; index++) { const current = polygon[index]; const previous = polygon[(index + polygon.length - 1) % polygon.length]; + // In range: both indexes are taken within the polygon's length. + if (!current || !previous) continue; const currentIn = inside(current); const previousIn = inside(previous); if (currentIn) { @@ -1187,18 +1194,20 @@ export class RenderComponent { for (let at = 0; at < points.length - 1; at++) { const from = points[at]; const to = points[at + 1]; + if (!from || !to) continue; const dx = to.x - from.x; const dy = to.y - from.y; let enter = 0; let leave = 1; let inside = true; - for (const [p, q] of [ + const edges: [p: number, q: number][] = [ [-dx, from.x - box.x1], [dx, box.x2 - from.x], [-dy, from.y - box.y1], [dy, box.y2 - from.y], - ]) { + ]; + for (const [p, q] of edges) { if (p === 0) { // Parallel to this edge: outside it means the whole segment is out. if (q < 0) { @@ -1219,8 +1228,9 @@ export class RenderComponent { const start = { x: from.x + dx * enter, y: from.y + dy * enter }; const end = { x: from.x + dx * leave, y: from.y + dy * leave }; - if (!run.length) run.push(start); - else if (!near(run[run.length - 1], start)) { + const last = run.at(-1); + if (!last) run.push(start); + else if (!near(last, start)) { if (run.length > 1) runs.push(run); run = [start]; } @@ -1266,8 +1276,10 @@ export class RenderComponent { const scale = this.pixels(edge.style('arrow-scale')) || 1; const size = Math.max(6, width * 4 * scale); - const tip = points[points.length - 1]; - const from = points[points.length - 2]; + const tip = points.at(-1); + const from = points.at(-2); + // Two points at least: checked on entry. + if (!tip || !from) return []; const run = Math.hypot(tip.x - from.x, tip.y - from.y) || 1; // Along the line, and across it. const ax = (tip.x - from.x) / run; @@ -1387,14 +1399,15 @@ export class RenderComponent { if (this.diagram()) { const { instances } = this.exportableInstances(); - if (!instances.length) throw new Error('no diagram to export'); + const [instance] = instances; + if (!instance) throw new Error('no diagram to export'); // One instance here by construction: this page never opens the // comparison view. // // full:false exports what the viewport shows, which is the point when the // viewport has been framed on one event. - const framed = this.frameSelection(instances[0]); - return instances[0].svg({ full: !framed }); + const framed = this.frameSelection(instance); + return instance.svg({ full: !framed }); } // Through the illustration's own service, which knows that its styling has @@ -1456,12 +1469,13 @@ export class RenderComponent { } const { diagram, instances } = this.exportableInstances(); - if (instances.length) { - const framed = this.frameSelection(instances[0]); + const [instance] = instances; + if (instance) { + const framed = this.frameSelection(instance); if (jpeg && diagram) { - return asJpeg(diagram.exportCanvas(instances[0], { full: !framed, scale, bg })); + return asJpeg(diagram.exportCanvas(instance, { full: !framed, scale, bg })); } - return instances[0].png({ full: !framed, scale, bg }); + return instance.png({ full: !framed, scale, bg }); } // An illustration has no cytoscape instance to ask, so it goes through the diff --git a/projects/pathway-browser/src/app/services/diagram.service.ts b/projects/pathway-browser/src/app/services/diagram.service.ts index 687655ee..fc1d9da0 100644 --- a/projects/pathway-browser/src/app/services/diagram.service.ts +++ b/projects/pathway-browser/src/app/services/diagram.service.ts @@ -63,10 +63,13 @@ const isFinitePoint = (point: Position | undefined | null): point is Position => const closestToAverage = (positions: Position[]): Position => { const average = avg(positions); - let closest = positions[0]; + const [first, ...rest] = positions; + // A shadow over a sub-pathway with no events: as before, the diagram cannot + // be laid out, now saying why rather than reading a property of undefined. + if (!first) throw new Error('There are no positions to choose a closest from.'); + let closest = first; let min = squaredDist(closest, average); - for (let i = 1; i < positions.length; i++) { - const pos = positions[i]; + for (const pos of rest) { const dist = squaredDist(pos, average); if (dist < min) { min = dist; @@ -169,7 +172,9 @@ export class DiagramService { } pick(values: T[]): T { - return values[this.random(0, values.length - 1)]; + if (values.length === 0) throw new Error('There is nothing to pick from.'); + // In range, and undefined is a value some callers pick on purpose. + return values[this.random(0, values.length - 1)] as T; } private readonly COMPARTMENT_SHIFT = 35; @@ -183,7 +188,7 @@ export class DiagramService { .get(`${CONTENT_SERVICE}/data/query/${id}/normalPathway`, { responseType: 'text', }) - .pipe(map((data) => data.split('\t')[0])); + .pipe(map((data) => data.split('\t')[0] ?? data)); } public getCHEBIStructure(ids: Set): Map> { @@ -258,7 +263,11 @@ export class DiagramService { node.standardIdentifier?.startsWith('chebi:') || node.chebiIdentifier !== undefined ) - .map((node) => (node.chebiIdentifier || node.standardIdentifier)!.split(':')[1]) + // An identifier without its "chebi:" prefix has no id after one. + .flatMap((node) => { + const [, id] = (node.chebiIdentifier || node.standardIdentifier)!.split(':'); + return id === undefined ? [] : [id]; + }) ) ), })), @@ -314,7 +323,7 @@ export class DiagramService { const eventIdToSubPathwayId = new Map( (graph.subpathways?.flatMap((subpathway) => subpathway.events - .map((event) => [event, subpathway.dbId]) + .map((event): [number, number] => [event, subpathway.dbId]) .filter((entry) => subpathwayIds.has(entry[1])) ) as [number, number][]) || [] ); @@ -331,10 +340,15 @@ export class DiagramService { ); const mappingList: [number, Graph.Node][] = graph.nodes .flatMap((node) => { - if (node.children?.length === 1 && node.diagramIds?.length !== 1) { + const [onlyChild] = node.children ?? []; + if ( + node.children?.length === 1 && + onlyChild !== undefined && + node.diagramIds?.length !== 1 + ) { // Consider homomer complex like their constituents for interactors return node.diagramIds - ?.map((id) => [id, dbIdToGraphNode.get(node.children[0])]) + ?.map((id) => [id, dbIdToGraphNode.get(onlyChild)]) .filter((entry) => entry[1] !== undefined) as [number, Graph.Node][]; } else { return node.diagramIds?.map((id) => [id, node]) as [number, Graph.Node][]; @@ -376,7 +390,8 @@ export class DiagramService { //compartment nodes const compartmentNodes: cytoscape.NodeDefinition[] = diagram?.compartments.flatMap((item) => { - const propToRects = (prop: Prop): { [p: string]: number } => ({ + const sides = ['left', 'top', 'right', 'bottom'] as const; + const propToRects = (prop: Prop): Record<(typeof sides)[number], number> => ({ left: scale(prop.x), top: scale(prop.y), right: scale(prop.x + prop.width), @@ -386,9 +401,9 @@ export class DiagramService { const innerCR = 10; let outerCR; if (item.insets) { - const rects = [propToRects(item.prop), propToRects(item.insets)]; - outerCR = Object.keys(rects[0]).reduce( - (smallest, key) => Math.min(smallest, Math.abs(rects[0][key] - rects[1][key])), + const [outer, inner] = [propToRects(item.prop), propToRects(item.insets)]; + outerCR = sides.reduce( + (smallest, key) => Math.min(smallest, Math.abs(outer[key] - inner[key])), Number.MAX_SAFE_INTEGER ); outerCR = innerCR + Math.min(outerCR, 100); @@ -490,7 +505,8 @@ export class DiagramService { let unitId = undefined; if (item.schemaClass === SchemaClasses.POLYMER) { const polymerGraphNode = dbIdToGraphNode.get(item.reactomeId)!; - const unitGraph = dbIdToGraphNode.get(polymerGraphNode.children[0])!; + // A unit the graph does not list reads as one it does not know, as before. + const unitGraph = dbIdToGraphNode.get(polymerGraphNode.children[0] ?? -1)!; const unitClass = this.nodeTypeMap.get(unitGraph.schemaClass) || this.nodeTypeMap.get( this.schemaClassToNodeTypeMap.get( @@ -754,13 +770,20 @@ export class DiagramService { }); const linkEdges: cytoscape.EdgeDefinition[] = diagram.links - ?.filter( - (link) => - !link.renderableClass.includes('EntitySet') || link.inputs[0].id !== link.outputs[0].id - ) + ?.filter((link) => { + const [input] = link.inputs; + const [output] = link.outputs; + // A link without both ends is kept, to be refused below by name. + if (!input || !output) return true; + return !link.renderableClass.includes('EntitySet') || input.id !== output.id; + }) ?.map((link) => { - const source = idToNodes.get(link.inputs[0].id)!; - const target = idToNodes.get(link.outputs[0].id)!; + const [input] = link.inputs; + const [output] = link.outputs; + // As before, a link with no end cannot be drawn; now it says which. + if (!input || !output) throw new Error(`Link ${link.id} has no input or no output.`); + const source = idToNodes.get(input.id)!; + const target = idToNodes.get(output.id)!; const sourceP = scale(source.position); const targetP = scale(target.position); @@ -781,14 +804,13 @@ export class DiagramService { if (link.isDisease) classes.push('disease'); const isBackground = link.isFadeOut || - (idToNodes.get(link.inputs[0].id)?.isBackground && - idToNodes.get(link.outputs[0].id)?.isBackground); + (idToNodes.get(input.id)?.isBackground && idToNodes.get(output.id)?.isBackground); return { data: { id: link.id + '', - source: link.inputs[0].id + '', - target: link.outputs[0].id + '', + source: input.id + '', + target: output.id + '', ...relativeSegments, ...this.getEndpointData(sourceP, targetP, from, to), isFadeOut: link.isFadeOut, @@ -1056,10 +1078,10 @@ export class DiagramService { classes: [this.pick(reactionTypes), 'reaction'], })); - const nodes: cytoscape.NodeDefinition[] = physicalEntities.flatMap((node, i) => [ - node, - reactions[i], - ]); + const nodes: cytoscape.NodeDefinition[] = physicalEntities.flatMap((node, i) => { + const reaction = reactions[i]; + return reaction ? [node, reaction] : [node]; + }); const inOut: cytoscape.EdgeDefinition[] = physicalEntities.flatMap((_node, i) => [ { diff --git a/projects/reactome-diagram-element/tsconfig.app.json b/projects/reactome-diagram-element/tsconfig.app.json index 28bf5940..88902e4e 100644 --- a/projects/reactome-diagram-element/tsconfig.app.json +++ b/projects/reactome-diagram-element/tsconfig.app.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/reactome-diagram-element", - "types": [] + "types": [], + // Every index read checked (#345). Not yet in tsconfig.json: the workspace + // libraries have findings left, held at a count by scripts/check-indexed-access.mjs. + "noUncheckedIndexedAccess": true }, "files": ["src/main.ts"], "include": ["../pathway-browser/src/**/*.d.ts", "../../src/**/*.d.ts"] diff --git a/scripts/check-indexed-access.mjs b/scripts/check-indexed-access.mjs index dd23adb6..26b0bda7 100644 --- a/scripts/check-indexed-access.mjs +++ b/scripts/check-indexed-access.mjs @@ -1,7 +1,12 @@ #!/usr/bin/env node /** * Hold the `noUncheckedIndexedAccess` findings at today's count, file by file, - * until there are none and the flag can go into tsconfig.json (#345). + * in the projects that do not have the flag yet (#345). + * + * The app and the embeddable diagram reached zero and have it in their own + * tsconfig.app.json, so their builds enforce it. What is left is the workspace + * libraries, listed in PROJECTS; when they reach zero too, the flag goes into + * tsconfig.json for everything and this check goes. * * With the flag, `arr[i]` and `record[key]` are typed `T | undefined`, which is * the honest type: an index out of range or a missing key really is undefined, @@ -14,9 +19,10 @@ * Moving or renaming a file with findings shows as an increase under its new * path. Check the total is unchanged, then run with --update. * - * It runs the project the build uses (tsconfig.app.json, as check-types does) - * with the flag added. A run that fails without naming a single finding is a - * crash, not a clean result, and is refused. + * It runs each project's own build config with the flag added. A run that + * fails without naming a single finding is a crash, not a clean result, and is + * refused. (tsconfig.spec.json is left out: it cannot resolve vitest/globals, + * as scripts/check-types.mjs says.) * * node scripts/check-indexed-access.mjs check against the baseline * node scripts/check-indexed-access.mjs --update record today's counts @@ -30,7 +36,10 @@ import { isEntry } from './is-entry.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const BASELINE = join(ROOT, 'indexed-access-baseline.json'); -const PROJECT = 'tsconfig.app.json'; +const PROJECTS = [ + 'projects/reactome-table/tsconfig.lib.json', + 'projects/reactome-gsa-form/tsconfig.lib.json', +]; /** `file(line,col): error TS…` lines, counted per file. */ export function countErrors(/** @type {string} */ output) { @@ -61,28 +70,45 @@ export function outcome(status, output) { return { ok: true, counts }; } +/** + * Several projects' counts as one: a file two projects compile is counted + * once, at the larger of its counts. + * @param {Record[]} perProject + */ +export function mergeCounts(perProject) { + /** @type {Record} */ + const merged = {}; + for (const counts of perProject) { + for (const [file, n] of Object.entries(counts)) merged[file] = Math.max(merged[file] ?? 0, n); + } + return merged; +} + const total = (/** @type {Record} */ counts) => Object.values(counts).reduce((a, b) => a + b, 0); -function main() { +/** One project's findings per file, or exits when tsc did not get that far. */ +function countProject(/** @type {string} */ project) { const result = spawnSync( 'npx', - ['tsc', '--noEmit', '--pretty', 'false', '-p', PROJECT, '--noUncheckedIndexedAccess'], + ['tsc', '--noEmit', '--pretty', 'false', '-p', project, '--noUncheckedIndexedAccess'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 } ); const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; const run = outcome(result.status, output); if (!run.ok) { console.error( - `tsc did not get as far as the type check (exit ${result.status}), so it has nothing to count:` + `tsc did not get as far as the type check of ${project} (exit ${result.status}), so it has nothing to count:` ); console.error(output.trim()); process.exit(1); } + return run.counts; +} - const counts = Object.fromEntries( - Object.entries(run.counts).sort(([a], [b]) => a.localeCompare(b)) - ); +function main() { + const merged = mergeCounts(PROJECTS.map(countProject)); + const counts = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b))); if (process.argv.includes('--update')) { writeFileSync( BASELINE, @@ -107,7 +133,7 @@ function main() { for (const { rule: file, was, now } of increased) console.error(` ${file}: ${was} -> ${now}`); console.error( `\nGuard the new index reads in those files: an index out of range or a missing key is` + - ` undefined. See them with:\n\n npx tsc --noEmit -p ${PROJECT} --noUncheckedIndexedAccess\n\n` + + ` undefined. See them with:\n\n${PROJECTS.map((project) => ` npx tsc --noEmit -p ${project} --noUncheckedIndexedAccess`).join('\n')}\n\n` + `A file that was only moved or renamed shows here under its new path: if the total` + ` (${total(counts)} now, ${total(baseline)} before) has not gone up, run with --update.\n` ); @@ -122,7 +148,9 @@ function main() { } else { console.log(`Unchecked indexed access: ${now}, unchanged from the baseline.`); } - if (now === 0) console.log('None left: add noUncheckedIndexedAccess to tsconfig.json.'); + if (now === 0) { + console.log('None left: add noUncheckedIndexedAccess to tsconfig.json, and remove this check.'); + } } if (isEntry(import.meta.url)) main(); diff --git a/tools/indexed-access-check/check-indexed-access.spec.mjs b/tools/indexed-access-check/check-indexed-access.spec.mjs index e82d1518..68c3f49a 100644 --- a/tools/indexed-access-check/check-indexed-access.spec.mjs +++ b/tools/indexed-access-check/check-indexed-access.spec.mjs @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { countErrors, outcome } from '../../scripts/check-indexed-access.mjs'; +import { countErrors, mergeCounts, outcome } from '../../scripts/check-indexed-access.mjs'; // tsc's own output, as `tsc --noEmit` prints it without --pretty. const output = [ @@ -36,4 +36,19 @@ describe('the indexed-access count', () => { expect(outcome(134, 'FATAL ERROR: JavaScript heap out of memory').ok).toBe(false); expect(outcome(2, 'error TS5058: The specified path does not exist').ok).toBe(false); }); + + // The projects still counted are separate libraries, but a file two of them + // compiled would be reported by both: once is the truth. + it('counts a file two projects compile once, at its larger count', () => { + expect( + mergeCounts([ + { 'a.ts': 2, 'b.ts': 1 }, + { 'b.ts': 3, 'c.ts': 1 }, + ]) + ).toEqual({ + 'a.ts': 2, + 'b.ts': 3, + 'c.ts': 1, + }); + }); }); diff --git a/tsconfig.app.json b/tsconfig.app.json index 8886e903..a8c19fe1 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,7 +4,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [], + // Every index read checked (#345). Not yet in tsconfig.json: the workspace + // libraries have findings left, held at a count by scripts/check-indexed-access.mjs. + "noUncheckedIndexedAccess": true }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] diff --git a/tsconfig.json b/tsconfig.json index 571416a9..86ece533 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,9 +51,11 @@ // Strictness here is a deliberate trade: each of these was measured against // the codebase before being switched on. strictTemplates cost 83 fixes and // was worth it; typeCheckHostBindings, strictStandalone and the checks below - // all cost nothing. noUncheckedIndexedAccess (185 errors) and - // exactOptionalPropertyTypes (72) are not on -- worth revisiting, but not - // free. noUncheckedIndexedAccess was trialled properly rather than dismissed: + // all cost nothing. exactOptionalPropertyTypes (72) is not on -- worth + // revisiting, but not free. noUncheckedIndexedAccess is on for the app and the + // embeddable diagram, in their own tsconfig.app.json (#345), and held at a + // count for the workspace libraries by scripts/check-indexed-access.mjs until + // it can go here. It was first trialled and set aside: // of its 185 findings, roughly 44% are places the code already checks .length // or iterates a bounded loop and TypeScript simply cannot connect the check to // the access, where the only "fix" is a non-null assertion that documents