diff --git a/biome.json b/biome.json index 6cdfa6f9..2d5bae48 100644 --- a/biome.json +++ b/biome.json @@ -34,11 +34,15 @@ "noImplicitAnyLet": "off", "noExplicitAny": "off", "noAssignInExpressions": "off", - "noVar": "error" + "noVar": "error", + "noEmptyBlockStatements": "warn", + "noMisplacedAssertion": "warn" }, "correctness": { "noUnusedPrivateClassMembers": "error", - "noUnusedImports": { "level": "warn" } + "noUnusedImports": { "level": "warn" }, + "noUnusedVariables": "error", + "noUnusedFunctionParameters": "error" }, "complexity": { "noExcessiveCognitiveComplexity": { @@ -46,12 +50,26 @@ "options": { "maxAllowedComplexity": 41 } - } + }, + "noUselessFragments": "error" }, "style": { "useConsistentArrayType": "error", "useArrayLiterals": "error", - "noNestedTernary": "error" + "noNestedTernary": "error", + "useImportType": "error", + "useExportType": "error", + "useThrowOnlyError": "error", + "useDefaultSwitchClause": "error", + "noEnum": "error", + "useFilenamingConvention": "error", + "noUselessElse": "warn" + }, + "performance": { + "useTopLevelRegex": "warn" + }, + "nursery": { + "noFloatingPromises": "warn" }, "recommended": true } @@ -69,6 +87,9 @@ "rules": { "style": { "noNonNullAssertion": "off" + }, + "performance": { + "useTopLevelRegex": "off" } } } diff --git a/src/api/builder.ts b/src/api/builder.ts index 1d306e76..f1f89fe0 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -437,7 +437,7 @@ export class APIBuilder { this.logger.debug(`Starting generation with ${this.generators.length} generators`); try { - if (this.options.cleanOutput) cleanup(this.options, this.logger); + if (this.options.cleanOutput) await cleanup(this.options, this.logger); let register: Register; if (this.prebuiltRegister) { diff --git a/src/api/writer-generator/csharp/csharp.ts b/src/api/writer-generator/csharp/csharp.ts index 61dc8ecf..7440f05a 100644 --- a/src/api/writer-generator/csharp/csharp.ts +++ b/src/api/writer-generator/csharp/csharp.ts @@ -19,9 +19,8 @@ const resolveCSharpAssets = (fn: string) => { const __dirname = Path.dirname(__filename); if (__filename.endsWith("dist/index.js")) { return Path.resolve(__dirname, "..", "assets", "api", "writer-generator", "csharp", fn); - } else { - return Path.resolve(__dirname, "../../../..", "assets", "api", "writer-generator", "csharp", fn); } + return Path.resolve(__dirname, "../../../..", "assets", "api", "writer-generator", "csharp", fn); }; const PRIMITIVE_TYPE_MAP: Record = { @@ -63,13 +62,15 @@ const formatBaseClass = (schema: SpecializationTypeSchema | NestedTypeSchema) => return schema.base ? `: ${schema.base.name}` : ""; }; +const LEADING_DIGIT_RE = /^\d/; + const canonicalToName = (canonical: string | undefined, dropFragment = true): string | undefined => { if (!canonical) return undefined; let localName = canonical.split("/").pop(); if (!localName) return undefined; if (dropFragment && localName.includes("#")) localName = localName.split("#")[0]; if (!localName) return undefined; - if (/^\d/.test(localName)) { + if (LEADING_DIGIT_RE.test(localName)) { localName = `number_${localName}`; } return formatName(localName); diff --git a/src/api/writer-generator/introspection.ts b/src/api/writer-generator/introspection.ts index 556c36ee..aff46339 100644 --- a/src/api/writer-generator/introspection.ts +++ b/src/api/writer-generator/introspection.ts @@ -66,7 +66,7 @@ export class IntrospectionWriter extends FileSystemWriter typeSchemaToJson(ts, true)); const seenFilenames = new Set(); @@ -125,9 +125,9 @@ export class IntrospectionWriter extends FileSystemWriter fhirSchemaToJson(fs, true)), outputPath, ); @@ -148,9 +148,9 @@ export class IntrospectionWriter extends FileSystemWriter structureDefinitionToJson(sd, true)), outputPath, ); diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts index 81b26744..a7d48bde 100644 --- a/src/api/writer-generator/mustache.ts +++ b/src/api/writer-generator/mustache.ts @@ -66,7 +66,9 @@ export function loadMustacheGeneratorConfig( if (parsed && typeof parsed === "object") { return parsed as Partial; } - } catch (_e) {} + } catch { + // Missing or invalid config.json: fall through to the warning below and return defaults. + } logger?.warn(`Failed to load JSON file with mustache generator config: ${filePath}`); return {}; } diff --git a/src/api/writer-generator/typescript/profile-extensions.ts b/src/api/writer-generator/typescript/profile-extensions.ts index a7fb9a5d..fc14d308 100644 --- a/src/api/writer-generator/typescript/profile-extensions.ts +++ b/src/api/writer-generator/typescript/profile-extensions.ts @@ -47,8 +47,10 @@ export const extractValueField = (elements: string[] | undefined): string | unde /** * Map a FHIR value field name (e.g. "valueCoding") to its TypeScript type. */ +const VALUE_PREFIX_RE = /^value/; + export const valueFieldToTsType = (valueField: string): string => { - const fhirName = valueField.replace(/^value/, ""); + const fhirName = valueField.replace(VALUE_PREFIX_RE, ""); // Primitive types that map to TS primitives const primitives: Record = { String: "string", diff --git a/src/api/writer-generator/utils.ts b/src/api/writer-generator/utils.ts index a2596da6..67b7e46c 100644 --- a/src/api/writer-generator/utils.ts +++ b/src/api/writer-generator/utils.ts @@ -1,5 +1,7 @@ +const WORD_SPLIT_RE = /(?<=[a-z])(?=[A-Z])|[-_.\s]/; + export const words = (s: string) => { - return s.split(/(?<=[a-z])(?=[A-Z])|[-_.\s]/).filter(Boolean); + return s.split(WORD_SPLIT_RE).filter(Boolean); }; export const kebabCase = (s: string) => { diff --git a/src/api/writer-generator/writer.ts b/src/api/writer-generator/writer.ts index dbad9b76..8a8c152e 100644 --- a/src/api/writer-generator/writer.ts +++ b/src/api/writer-generator/writer.ts @@ -215,9 +215,8 @@ export abstract class Writer extends Fi tokens = tokens.map((token) => { if (typeof token === "string") { return token; - } else { - return JSON.stringify(token, null, 2); } + return JSON.stringify(token, null, 2); }); this.comment(...tokens); } diff --git a/src/typeschema/core/field-builder.ts b/src/typeschema/core/field-builder.ts index ab7de8bd..984e178c 100644 --- a/src/typeschema/core/field-builder.ts +++ b/src/typeschema/core/field-builder.ts @@ -310,7 +310,8 @@ export function buildFieldType( .slice(1) // drop canonicalUrl .filter((_, i) => i % 2 === 1); // drop `elements` from path return mkNestedIdentifier(register, fhirSchema, refPath); - } else if (element.type) { + } + if (element.type) { const url = register.ensureSpecializationCanonicalUrl(element.type); const fieldFs = register.resolveFs(fhirSchema.package_meta, url); if (!fieldFs) { @@ -326,19 +327,20 @@ export function buildFieldType( ); } return mkIdentifier(fieldFs); - } else if (element.choices) { + } + if (element.choices) { return undefined; - } else if (fhirSchema.derivation === "constraint") { + } + if (fhirSchema.derivation === "constraint") { return undefined; // FIXME: should be removed - } else { - // Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions - // Log a warning but continue processing instead of throwing - logger?.dryWarn( - "#fieldTypeNotFound", - `Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`, - ); - return undefined; } + // Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions + // Log a warning but continue processing instead of throwing + logger?.dryWarn( + "#fieldTypeNotFound", + `Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`, + ); + return undefined; } export const mkField = ( diff --git a/src/typeschema/core/identifier.ts b/src/typeschema/core/identifier.ts index 30f5164c..55871063 100644 --- a/src/typeschema/core/identifier.ts +++ b/src/typeschema/core/identifier.ts @@ -63,13 +63,16 @@ export function mkIdentifier(fhirSchema: RichFHIRSchema): Identifier { return { kind: "resource", ...fields }; } +const VALUE_SET_NAME_SPLIT_RE = /[-_]/; +const OPAQUE_VALUE_SET_ID_RE = /^[a-zA-Z0-9_-]{20,}$/; + const getValueSetName = (url: CanonicalUrl): Name => { const urlParts = url.split("/"); const lastSegment = urlParts[urlParts.length - 1]; if (lastSegment && lastSegment.length > 0) { return lastSegment - .split(/[-_]/) + .split(VALUE_SET_NAME_SPLIT_RE) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join("") as Name; } @@ -97,7 +100,7 @@ export function mkValueSetIdentifierByUrl( const valueSet: RichValueSet = register.resolveVs(pkg, valueSetUrl) || valuesSetFallback; // NOTE: ignore valueSet.name due to human name const valueSetName: Name = - valueSet?.id && !/^[a-zA-Z0-9_-]{20,}$/.test(valueSet.id) ? (valueSet.id as Name) : valueSetNameFallback; + valueSet?.id && !OPAQUE_VALUE_SET_ID_RE.test(valueSet.id) ? (valueSet.id as Name) : valueSetNameFallback; return { kind: "value-set", diff --git a/src/typeschema/ir/tree-shake.ts b/src/typeschema/ir/tree-shake.ts index aafbceec..01104b4b 100644 --- a/src/typeschema/ir/tree-shake.ts +++ b/src/typeschema/ir/tree-shake.ts @@ -28,9 +28,8 @@ import type { IrReport, TreeShakeConf, TreeShakeReport, TreeShakeRule } from "./ const ensureIrReport = (indexOrReport: TypeSchemaIndex | IrReport): IrReport => { if ("irReport" in indexOrReport && typeof indexOrReport.irReport === "function") { return indexOrReport.irReport(); - } else { - return indexOrReport as IrReport; } + return indexOrReport as IrReport; }; export const rootTreeShakeReadme = (report: TypeSchemaIndex | IrReport) => { diff --git a/src/typeschema/register.ts b/src/typeschema/register.ts index cf45d674..1aff3647 100644 --- a/src/typeschema/register.ts +++ b/src/typeschema/register.ts @@ -18,6 +18,8 @@ import type { } from "@typeschema/types"; import { enrichFHIRSchema, enrichValueSet, packageMetaToFhir, packageMetaToNpm } from "@typeschema/types"; +const BARE_RESOURCE_NAME_RE = /^[a-zA-Z0-9]+$/; + export type Register = { testAppendFs(fs: FHIRSchema): void; ensureSpecializationCanonicalUrl(name: string | Name | CanonicalUrl): CanonicalUrl; @@ -224,7 +226,7 @@ export const registerFromManager = async ( const ensureSpecializationCanonicalUrl = (name: string | Name | CanonicalUrl): CanonicalUrl => { // Strip version suffix from canonical URL (e.g., "Extension|4.0.1" -> "Extension") if (name.includes("|")) name = name.split("|")[0] as CanonicalUrl; - if (name.match(/^[a-zA-Z0-9]+$/)) { + if (BARE_RESOURCE_NAME_RE.test(name)) { return `http://hl7.org/fhir/StructureDefinition/${name}` as CanonicalUrl; } return name as CanonicalUrl; diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 05302bd9..ab712150 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -10,6 +10,8 @@ import type { StructureDefinition, ValueSet, ValueSetCompose } from "@root/fhir- export type Name = string & { readonly __brand: unique symbol }; export type CanonicalUrl = string & { readonly __brand: unique symbol }; +const LEADING_DIGIT_RE = /^\d/; + export const extractNameFromCanonical = (canonical: CanonicalUrl, dropFragment = true) => { let localName = canonical.split("/").pop(); if (!localName) return undefined; @@ -17,7 +19,7 @@ export const extractNameFromCanonical = (canonical: CanonicalUrl, dropFragment = localName = localName.split("#")[0]; } if (!localName) return undefined; - if (/^\d/.test(localName)) { + if (LEADING_DIGIT_RE.test(localName)) { localName = `number_${localName}`; } return localName;