diff --git a/src/domain/javascript/javascriptApplicationEvidenceSchemas.ts b/src/domain/javascript/javascriptApplicationEvidenceSchemas.ts index 73f3798952..34d5dd2bb3 100644 --- a/src/domain/javascript/javascriptApplicationEvidenceSchemas.ts +++ b/src/domain/javascript/javascriptApplicationEvidenceSchemas.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { javascriptSourceRangeSchema } from "./javascriptSourceCoordinates.js"; import { digestSchema } from "../digests.js"; import { prefixedDigestSchema } from "../digests.js"; @@ -87,31 +88,10 @@ const applicationArtifactReferenceSchema = z.discriminatedUnion("available", [ unavailableFactSchema, ]); -const sourceRangeSchema = z - .strictObject({ - kind: z.literal("source-range"), - source: boundedTextSchema, - start: z.strictObject({ - line: z.number().int().min(1), - column: z.number().int().min(0), - }), - end: z.strictObject({ - line: z.number().int().min(1), - column: z.number().int().min(0), - }), - }) - .superRefine((location, context) => { - if ( - location.end.line < location.start.line || - (location.end.line === location.start.line && - location.end.column < location.start.column) - ) - context.addIssue({ - code: "custom", - path: ["end"], - message: "Source range end must not precede its start", - }); - }); +const sourceRangeSchema = javascriptSourceRangeSchema.safeExtend({ + kind: z.literal("source-range"), + source: boundedTextSchema, +}); const fileOffsetRangeSchema = z .strictObject({ diff --git a/src/domain/javascript/javascriptApplicationGraph.test.ts b/src/domain/javascript/javascriptApplicationGraph.test.ts index 5bf9174809..5d4337406a 100644 --- a/src/domain/javascript/javascriptApplicationGraph.test.ts +++ b/src/domain/javascript/javascriptApplicationGraph.test.ts @@ -815,6 +815,24 @@ describe("JavaScript Application Graph: identity strategy requirements", () => { ); }); + it.each([ + { start: { line: 2, column: 0 }, end: { line: 1, column: 9 } }, + { start: { line: 1, column: 9 }, end: { line: 1, column: 0 } }, + ])("rejects reversed source locations on graph facts: %j", (range) => { + const evidence = artifactEvidence( + APPLICATION_GRAPH_DIGESTS.asar, + "dist/module.js", + "ast-static-analysis", + ); + evidence.location = { + available: true, + value: { kind: "source-range", source: "dist/module.js", ...range }, + }; + expect(() => graphForNode(contentNode(evidence))).toThrow( + /Source range end must not precede its start/u, + ); + }); + it("prevents static authorities from claiming runtime locations", () => { const evidence = artifactEvidence( APPLICATION_GRAPH_DIGESTS.asar, diff --git a/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts b/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts index b7ac7ee4f5..593aae4d3b 100644 --- a/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts +++ b/src/domain/javascript/javascriptExportShapeComparisonSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { javascriptSourceRangeSchema } from "./javascriptSourceCoordinates.js"; + import { evidenceSchema } from "../evidence.js"; import { prefixedDigestSchema } from "../digests.js"; @@ -31,29 +33,6 @@ const jsonPointerSchema = z .string() .refine(isJsonPointer, "Expected an RFC 6901 JSON Pointer"); -const sourcePointSchema = z.strictObject({ - line: z.number().int().min(1), - column: z.number().int().min(0), -}); - -const sourceRangeSchema = z - .strictObject({ - start: sourcePointSchema, - end: sourcePointSchema, - }) - .superRefine((range, context) => { - if ( - range.end.line < range.start.line || - (range.end.line === range.start.line && - range.end.column < range.start.column) - ) - context.addIssue({ - code: "custom", - path: ["end"], - message: "Source range end must not precede its start", - }); - }); - /** Check that export-shape comparisons bind distinct source Evidence. */ const validateJavaScriptExportShapeIdentities = ( input: { @@ -136,7 +115,7 @@ export type ProjectedPropertyCoverage = z.infer< const projectedReturnShapeSchema = z .strictObject({ - source_range: sourceRangeSchema, + source_range: javascriptSourceRangeSchema, value_status: z.enum([ "literal", "union", @@ -269,8 +248,8 @@ export const javaScriptExportShapeComparisonChangeSchema = z.strictObject({ }), left: valueAvailabilitySchema, right: valueAvailabilitySchema, - left_source_range: sourceRangeSchema.nullable(), - right_source_range: sourceRangeSchema.nullable(), + left_source_range: javascriptSourceRangeSchema.nullable(), + right_source_range: javascriptSourceRangeSchema.nullable(), evidence_links: z.array(evidenceIdSchema).length(2), limitations: z.array(textSchema), }); @@ -285,7 +264,7 @@ const propertyInventorySchema = z.strictObject({ variant_index: z.number().int().nonnegative(), discriminant: discriminantSchema.nullable(), paired: z.boolean(), - source_range: sourceRangeSchema, + source_range: javascriptSourceRangeSchema, properties: z.array(jsonPointerSchema), property_coverage: z.array(inventoryPropertyCoverageSchema), }); diff --git a/src/domain/javascript/javascriptSemanticGraphSchemas.ts b/src/domain/javascript/javascriptSemanticGraphSchemas.ts index b7cefc1025..62d27a6cdb 100644 --- a/src/domain/javascript/javascriptSemanticGraphSchemas.ts +++ b/src/domain/javascript/javascriptSemanticGraphSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { javascriptSourceRangeSchema } from "./javascriptSourceCoordinates.js"; + import { applicationGraphEvidenceContextSchema, applicationGraphEvidenceLocationSchema, @@ -188,32 +190,12 @@ const relativePathSchema = z "Expected a normalized relative POSIX path without traversal", ); -const sourcePointSchema = z.strictObject({ - line: z.number().int().min(1), - column: z.number().int().min(0), -}); - -const sourceRangeSchema = z - .strictObject({ start: sourcePointSchema, end: sourcePointSchema }) - .superRefine((range, context) => { - if ( - range.end.line < range.start.line || - (range.end.line === range.start.line && - range.end.column < range.start.column) - ) - context.addIssue({ - code: "custom", - path: ["end"], - message: "Source range end must not precede its start", - }); - }); - const semanticPropertiesSchema = z.record(z.string().min(1), jsonValueSchema); const semanticNodeIdentitySchema = z.strictObject({ artifact_sha256: digestSchema, module_path: relativePathSchema, - source_range: sourceRangeSchema.nullable(), + source_range: javascriptSourceRangeSchema.nullable(), role_key: z.string().min(1), }); diff --git a/src/domain/javascript/javascriptSourceCoordinates.ts b/src/domain/javascript/javascriptSourceCoordinates.ts new file mode 100644 index 0000000000..f9c3978185 --- /dev/null +++ b/src/domain/javascript/javascriptSourceCoordinates.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +const sourcePointSchema = z.strictObject({ + line: z.number().int().min(1), + column: z.number().int().min(0), +}); + +/** JavaScript source coordinates use one-based lines and zero-based columns. */ +export const javascriptSourceRangeSchema = z + .strictObject({ start: sourcePointSchema, end: sourcePointSchema }) + .superRefine((range, context) => { + if ( + range.end.line < range.start.line || + (range.end.line === range.start.line && + range.end.column < range.start.column) + ) + context.addIssue({ + code: "custom", + path: ["end"], + message: "Source range end must not precede its start", + }); + });