From 0d9600b1ece01941a21ce341d0e92e9e0d0be456 Mon Sep 17 00:00:00 2001 From: Victor Lin <13424970+victorlin@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:25:20 -0800 Subject: [PATCH] Remove types for variables derived from JSON input These types had ignored the reality that the JSON input can be anything. The removal better aligns with the validation that is already present in the functions. More validation has been added to accommodate for newly exposed type errors. --- src/util/entropyCreateStateFromJsons.ts | 91 +++++++++++++++---------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/src/util/entropyCreateStateFromJsons.ts b/src/util/entropyCreateStateFromJsons.ts index 9beeab14e..4e79dbfb1 100644 --- a/src/util/entropyCreateStateFromJsons.ts +++ b/src/util/entropyCreateStateFromJsons.ts @@ -1,30 +1,11 @@ import { genotypeColors } from "./globals"; import { defaultEntropyState } from "../reducers/entropy"; -type JsonAnnotations = Record - -type Strand = '+' | '-' // other GFF-valid options are '.' and '?' - -interface JsonSegmentRange { - /** 1-based */ - start: number - - /** 1-based closed (GFF) */ - end: number +interface UnknownJsonObject { + [key: string]: unknown } -interface JsonAnnotation { - /* Other properties are commonly set in the JSON structure, but the following are - the only ones read by Auspice */ - end?: number - start?: number - segments?: JsonSegmentRange[] - strand?: Strand - gene?: string - color?: string - display_name?: string - description?: string -} +type Strand = '+' | '-' // other GFF-valid options are '.' and '?' /** * Specifies the range of the each segment's corresponding position in the genome, @@ -110,24 +91,52 @@ interface CdsSegment { * ¹ The exception being a single CDS which wraps around the origin, which we are able * to split into two segments here. */ -export const genomeMap = (annotations: JsonAnnotations): Chromosome[] => { +export const genomeMap = (annotations: UnknownJsonObject): Chromosome[] => { const nucAnnotation = Object.entries(annotations) .filter(([name,]) => name==='nuc') .map(([, annotation]) => annotation)[0]; - if (!nucAnnotation) throw new Error("Genome annotation missing 'nuc' definition") - if (!nucAnnotation.start || !nucAnnotation.end) throw new Error("Genome annotation for 'nuc' missing start or end") - if (nucAnnotation.strand==='-') throw new Error("Auspice can only display genomes represented as positive strand." + - "Note that -ve strand RNA viruses are typically annotated as 5' → 3'."); + + if (!nucAnnotation) { + throw new Error("Genome annotation missing 'nuc' definition"); + } + if (typeof nucAnnotation !== 'object') { + throw new Error("Genome annotation for 'nuc' is not a JSON object."); + } + if (!('start' in nucAnnotation) || !('end' in nucAnnotation)) { + throw new Error("Genome annotation for 'nuc' missing start or end"); + } + if (typeof nucAnnotation.start !== 'number' || typeof nucAnnotation.end !== 'number') { + throw new Error("Genome annotation for 'nuc.start' or 'nuc.end' is not a number."); + } + if (!('strand' in nucAnnotation)) { + throw new Error("Genome annotation for 'nuc' missing strand"); + } + if (nucAnnotation.strand === '-') { + throw new Error("Auspice can only display genomes represented as positive strand." + + "Note that -ve strand RNA viruses are typically annotated as 5' → 3'."); + } + const rangeGenome: RangeGenome = [nucAnnotation.start, nucAnnotation.end]; /* Group by genes -- most JSONs will not include this information, so it'll essentially be one CDS per gene, but that's just fine! */ - const annotationsPerGene: Record = {}; + const annotationsPerGene: Record> = {}; Object.entries(annotations) .filter(([name,]) => name!=='nuc') .map(([annotationKey, annotation]) => { + + if (typeof annotation !== 'object') { + throw new Error(`Genome annotation for '${annotationKey}' is not a JSON object.`); + } + if (!('gene' in annotation)) { + throw new Error(`Genome annotation for '${annotationKey}' missing gene.`); + } + if (typeof annotation.gene !== 'string') { + throw new Error(`Genome annotation '${annotationKey}.gene' is not a string.`); + } + const geneName = annotation.gene || annotationKey; if (!(geneName in annotationsPerGene)) annotationsPerGene[geneName] = {}; annotationsPerGene[geneName][annotationKey] = annotation; @@ -167,7 +176,7 @@ export const genomeMap = (annotations: JsonAnnotations): Chromosome[] => { return [chromosome]; } -export const entropyCreateState = (genomeAnnotations: JsonAnnotations) => { +export const entropyCreateState = (genomeAnnotations: UnknownJsonObject) => { if (genomeAnnotations) { try { return { @@ -185,8 +194,8 @@ export const entropyCreateState = (genomeAnnotations: JsonAnnotations) => { }; -function validColor(color: string | undefined) { - if (!color) return false; +function validColor(color: string | undefined | unknown) { + if (typeof color !== "string") return false; return color; // TODO XXX } @@ -203,7 +212,7 @@ function* nextColorGenerator() { */ function cdsFromAnnotation( cdsName: string, - annotation: JsonAnnotation, + annotation: UnknownJsonObject, rangeGenome: RangeGenome, defaultColor: string | void, ): CDS { @@ -230,6 +239,12 @@ function cdsFromAnnotation( let length = 0; // rangeLocal length const segments: CdsSegment[] = []; if (annotation.start && annotation.end) { + + if (typeof annotation.start !== 'number' || typeof annotation.end !== 'number') { + console.error(`[Genome annotation] ${cdsName} start (${annotation.start}) and/or end (${annotation.end}) is not a number.`); + return invalidCds; + } + /* The simplest case is where a JSON annotation block defines a contiguous CDS, however it may be a wrapping CDS (i.e. cds end > genome end */ @@ -248,8 +263,12 @@ function cdsFromAnnotation( {start: annotation.start, end: rangeGenome[1]}, {start: 1, end: annotation.end-rangeGenome[1]} ] - /* -ve strand segments are 3' -> 5', so segment[0] is at the start of the genome */ - if (!positive) annotation.segments.reverse(); + // TypeScript is unable to infer that annotation.segments is an array, + // hence the explicit type guard. + if (Array.isArray(annotation.segments)){ + /* -ve strand segments are 3' -> 5', so segment[0] is at the start of the genome */ + if (!positive) annotation.segments.reverse(); + } } } @@ -298,10 +317,10 @@ function cdsFromAnnotation( isWrapping: _isCdsWrapping(strand, segments), color: validColor(annotation.color) || defaultColor || '#000', } - if (annotation.display_name !== undefined) { + if (typeof annotation.display_name === 'string') { cds.displayName = annotation.display_name; } - if (annotation.description !== undefined) { + if (typeof annotation.description === 'string') { cds.description = annotation.description; } return cds