From b36313019759c8150c8325965d76288001b04475 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 18:40:09 +0000 Subject: [PATCH 1/4] Initial plan From 90c90e59f9de7dc73b6136bb7fea4131e1b14c27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 18:49:18 +0000 Subject: [PATCH 2/4] Fix error message truncation in TypeScript validator Two improvements to diagnostic message handling in createTypeScriptJsonValidator: 1. Use ts.flattenDiagnosticMessageText() instead of manually extracting d.messageText.messageText to properly handle DiagnosticMessageChain messages with nested detail. 2. For TypeScript error 2740 (missing properties truncated with "and N more"), use the type checker to reconstruct the full list of missing required properties rather than relying on the compiler's hardcoded 4-property limit. Agent-Logs-Url: https://github.com/microsoft/TypeChat/sessions/bc17d936-9d71-49f6-9fe7-d0a6d9d46770 Co-authored-by: robgruen <25374553+robgruen@users.noreply.github.com> --- typescript/src/ts/validate.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/typescript/src/ts/validate.ts b/typescript/src/ts/validate.ts index 89aebffa..c72fe042 100644 --- a/typescript/src/ts/validate.ts +++ b/typescript/src/ts/validate.ts @@ -58,12 +58,44 @@ export function createTypeScriptJsonValidator(schema: const syntacticDiagnostics = program.getSyntacticDiagnostics(); const programDiagnostics = syntacticDiagnostics.length ? syntacticDiagnostics : program.getSemanticDiagnostics(); if (programDiagnostics.length) { - const diagnostics = programDiagnostics.map(d => typeof d.messageText === "string" ? d.messageText : d.messageText.messageText).join("\n"); + const checker = program.getTypeChecker(); + const diagnostics = programDiagnostics.map(d => { + const message = ts.flattenDiagnosticMessageText(d.messageText, "\n"); + // TS error 2740 truncates the missing-properties list to 4 items ("and N more"). + // Use the type checker to reconstruct the full list of missing required properties. + if (d.code === 2740 && d.file) { + return expandMissingPropertiesMessage(checker, d.file) ?? message; + } + return message; + }).join("\n"); return error(diagnostics); } return success(jsonObject as T); } + function expandMissingPropertiesMessage(checker: ts.TypeChecker, file: ts.SourceFile): string | undefined { + for (const stmt of file.statements) { + if (ts.isVariableStatement(stmt)) { + for (const decl of stmt.declarationList.declarations) { + if (decl.type && ts.isTypeReferenceNode(decl.type) && decl.initializer) { + const targetType = checker.getTypeAtLocation(decl.type); + const sourceType = checker.getTypeAtLocation(decl.initializer); + const sourceProps = new Set(sourceType.getProperties().map(p => p.name)); + const missingProps = targetType.getProperties() + .filter(p => !(p.flags & ts.SymbolFlags.Optional) && !sourceProps.has(p.name)) + .map(p => p.name); + if (missingProps.length > 0) { + const srcStr = checker.typeToString(sourceType, undefined, ts.TypeFormatFlags.NoTruncation); + const tgtStr = checker.typeToString(targetType, undefined, ts.TypeFormatFlags.NoTruncation); + return `Type '${srcStr}' is missing the following properties from type '${tgtStr}': ${missingProps.join(", ")}`; + } + } + } + } + } + return undefined; + } + function createModuleTextFromJson(jsonObject: object) { return success(`import { ${typeName} } from './schema';\nconst json: ${typeName} = ${JSON.stringify(jsonObject, undefined, 2)};\n`); } From b21e3d1c06b80196f9dcfa1ef7f13b634dfbc962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 18:52:45 +0000 Subject: [PATCH 3/4] Refine expandMissingPropertiesMessage: use getStart() and add JSDoc - Use decl.getStart(file) instead of decl.pos to exclude leading trivia from the position range check, making it more accurate - Add a JSDoc comment explaining the function's purpose and return value Agent-Logs-Url: https://github.com/microsoft/TypeChat/sessions/bc17d936-9d71-49f6-9fe7-d0a6d9d46770 Co-authored-by: robgruen <25374553+robgruen@users.noreply.github.com> --- typescript/src/ts/validate.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/typescript/src/ts/validate.ts b/typescript/src/ts/validate.ts index c72fe042..02e6a13a 100644 --- a/typescript/src/ts/validate.ts +++ b/typescript/src/ts/validate.ts @@ -63,8 +63,8 @@ export function createTypeScriptJsonValidator(schema: const message = ts.flattenDiagnosticMessageText(d.messageText, "\n"); // TS error 2740 truncates the missing-properties list to 4 items ("and N more"). // Use the type checker to reconstruct the full list of missing required properties. - if (d.code === 2740 && d.file) { - return expandMissingPropertiesMessage(checker, d.file) ?? message; + if (d.code === 2740 && d.file && d.start !== undefined) { + return expandMissingPropertiesMessage(checker, d.file, d.start) ?? message; } return message; }).join("\n"); @@ -73,11 +73,20 @@ export function createTypeScriptJsonValidator(schema: return success(jsonObject as T); } - function expandMissingPropertiesMessage(checker: ts.TypeChecker, file: ts.SourceFile): string | undefined { + /** + * For TypeScript error 2740 (missing required properties, truncated with "and N more"), + * uses the type checker to compute the full list of missing required properties from the + * variable declaration at `position` in `file`. Returns `undefined` if the declaration + * cannot be located or yields no missing properties (fallback to the original message). + */ + function expandMissingPropertiesMessage(checker: ts.TypeChecker, file: ts.SourceFile, position: number): string | undefined { for (const stmt of file.statements) { if (ts.isVariableStatement(stmt)) { for (const decl of stmt.declarationList.declarations) { - if (decl.type && ts.isTypeReferenceNode(decl.type) && decl.initializer) { + // Match the specific declaration that spans the diagnostic position. + // Use getStart() to exclude leading trivia from the range check. + if (decl.getStart(file) <= position && position <= decl.end && + decl.type && ts.isTypeReferenceNode(decl.type) && decl.initializer) { const targetType = checker.getTypeAtLocation(decl.type); const sourceType = checker.getTypeAtLocation(decl.initializer); const sourceProps = new Set(sourceType.getProperties().map(p => p.name)); From 82493052c61d045597238711542354ab9d65b769 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 19:14:33 +0000 Subject: [PATCH 4/4] Add unit tests for TypeScript validator and npm test script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test/validate.test.ts with 12 tests using node:test / node:assert: * Valid objects passing validation (all three cases) * Type mismatch error (backward-compat, code != 2740) * Single missing property (TS 2741, backward-compat) * 2-5 missing properties (TS 2739, backward-compat) * 6+ missing required properties (TS 2740) — verifies full list with no "and N more" truncation * Optional properties excluded from missing-props list * getSchemaText/getTypeName pass-through * createModuleTextFromJson output - Add test/tsconfig.json (compiles to out/, which is gitignored) - Add "test" script to package.json: tsc -p test && node --test out/validate.test.js Agent-Logs-Url: https://github.com/microsoft/TypeChat/sessions/b6e951eb-19b1-47a0-bce8-e447ac9a4efe Co-authored-by: TalZaccai <18443527+TalZaccai@users.noreply.github.com> --- typescript/package.json | 1 + typescript/test/tsconfig.json | 15 +++ typescript/test/validate.test.ts | 196 +++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 typescript/test/tsconfig.json create mode 100644 typescript/test/validate.test.ts diff --git a/typescript/package.json b/typescript/package.json index 902d08b2..a6f4f9d4 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -22,6 +22,7 @@ "scripts": { "build": "tsc -p src", "build-all": "npm run build --workspaces", + "test": "tsc -p test && node --test out/validate.test.js", "prepare": "npm run build-all", "prepublishOnly": "node -e \"require('fs').copyFileSync('../SECURITY.md','SECURITY.md')\"", "postpublish": "node -e \"require('fs').unlinkSync('SECURITY.md')\"", diff --git a/typescript/test/tsconfig.json b/typescript/test/tsconfig.json new file mode 100644 index 00000000..c98e759b --- /dev/null +++ b/typescript/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["es2021"], + "module": "node16", + "moduleResolution": "node16", + "types": ["node"], + "esModuleInterop": true, + "outDir": "../out", + "rootDir": ".", + "skipLibCheck": true, + "strict": true + }, + "include": ["./**/*.ts"] +} diff --git a/typescript/test/validate.test.ts b/typescript/test/validate.test.ts new file mode 100644 index 00000000..31ef89fa --- /dev/null +++ b/typescript/test/validate.test.ts @@ -0,0 +1,196 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { createTypeScriptJsonValidator } from "../dist/ts/index.js"; + +// --------------------------------------------------------------------------- +// Shared schemas +// --------------------------------------------------------------------------- + +// Schema with 8 required properties — enough to trigger TS error 2740 +// ("and N more") when several are absent. +const largeSchema = ` +export interface Doc { + id: number; + title: string; + slug: string; + abstract: string; + description: string; + author: string; + date: string; + category: string; +}`; + +// Schema with a mix of required and optional properties. +const mixedSchema = ` +export interface Item { + id: number; + name: string; + tag?: string; + extra1: string; + extra2: string; + extra3: string; + extra4: string; + extra5: string; +}`; + +// Small schema for simpler error cases. +const smallSchema = ` +export interface Point { + x: number; + y: number; +}`; + +// --------------------------------------------------------------------------- +// Helper +// --------------------------------------------------------------------------- +function validatorFor(schema: string, typeName: string) { + return createTypeScriptJsonValidator(schema, typeName); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("createTypeScriptJsonValidator", () => { + + describe("valid objects", () => { + it("accepts an object that satisfies all required properties", () => { + const v = validatorFor(largeSchema, "Doc"); + const result = v.validate({ + id: 1, title: "T", slug: "s", abstract: "a", + description: "d", author: "au", date: "2024-01-01", category: "c" + }); + assert.ok(result.success, "expected validation to succeed"); + }); + + it("accepts an object that supplies optional properties", () => { + const v = validatorFor(mixedSchema, "Item"); + const result = v.validate({ + id: 1, name: "n", tag: "t", + extra1: "a", extra2: "b", extra3: "c", extra4: "d", extra5: "e" + }); + assert.ok(result.success, "expected validation to succeed with optional props"); + }); + + it("accepts an object that omits optional properties", () => { + const v = validatorFor(mixedSchema, "Item"); + const result = v.validate({ + id: 1, name: "n", + extra1: "a", extra2: "b", extra3: "c", extra4: "d", extra5: "e" + }); + assert.ok(result.success, "expected validation to succeed without optional props"); + }); + }); + + describe("type mismatch errors (backward-compatible: unchanged code path)", () => { + it("reports a type error when a property has the wrong type", () => { + const v = validatorFor(smallSchema, "Point"); + const result = v.validate({ x: "not-a-number", y: 2 }); + assert.ok(!result.success, "expected validation to fail"); + assert.ok( + result.message.includes("not assignable to type"), + `expected type-mismatch message, got: ${result.message}` + ); + }); + }); + + describe("single missing required property (TS error 2741, unchanged code path)", () => { + it("reports the missing property by name", () => { + const v = validatorFor(smallSchema, "Point"); + const result = v.validate({ x: 1 }); + assert.ok(!result.success, "expected validation to fail"); + assert.ok( + result.message.includes("'y'") && result.message.includes("missing"), + `expected missing-property message, got: ${result.message}` + ); + }); + }); + + describe("2–5 missing required properties (TS error 2739, unchanged code path)", () => { + it("lists all missing properties without truncation for 4 missing", () => { + const schema = `export interface T { a: number; b: string; c: boolean; d: number; e: string; }`; + const v = validatorFor(schema, "T"); + // Provide only 'a', missing b/c/d/e + const result = v.validate({ a: 1 }); + assert.ok(!result.success, "expected validation to fail"); + for (const prop of ["b", "c", "d", "e"]) { + assert.ok( + result.message.includes(prop), + `expected '${prop}' in error message, got: ${result.message}` + ); + } + }); + }); + + describe("6+ missing required properties (TS error 2740 — the truncation fix)", () => { + it("returns the full list of missing properties without truncation", () => { + const v = validatorFor(largeSchema, "Doc"); + // Provide only id and title; slug/abstract/description/author/date/category are missing + const result = v.validate({ id: 1, title: "Hello" }); + assert.ok(!result.success, "expected validation to fail"); + + const missing = ["slug", "abstract", "description", "author", "date", "category"]; + for (const prop of missing) { + assert.ok( + result.message.includes(prop), + `expected '${prop}' in error message but got: ${result.message}` + ); + } + assert.ok( + !result.message.includes("more"), + `error message should not contain "more" (truncation indicator), got: ${result.message}` + ); + }); + + it("excludes optional properties from the missing-properties list", () => { + const v = validatorFor(mixedSchema, "Item"); + // Provide only id — name/extra1..5 are missing required; tag is optional + const result = v.validate({ id: 1 }); + assert.ok(!result.success, "expected validation to fail"); + + const requiredMissing = ["name", "extra1", "extra2", "extra3", "extra4", "extra5"]; + for (const prop of requiredMissing) { + assert.ok( + result.message.includes(prop), + `expected '${prop}' in error message, got: ${result.message}` + ); + } + assert.ok( + !result.message.includes("tag"), + `optional property 'tag' should not appear in missing-properties message, got: ${result.message}` + ); + }); + + it("does not include 'and N more' truncation text", () => { + const v = validatorFor(largeSchema, "Doc"); + const result = v.validate({}); + assert.ok(!result.success, "expected validation to fail"); + assert.ok( + !/ and \d+ more/.test(result.message), + `message should not contain "and N more", got: ${result.message}` + ); + }); + }); + + describe("getSchemaText and getTypeName", () => { + it("returns the original schema text", () => { + const v = validatorFor(smallSchema, "Point"); + assert.strictEqual(v.getSchemaText(), smallSchema); + }); + + it("returns the correct type name", () => { + const v = validatorFor(smallSchema, "Point"); + assert.strictEqual(v.getTypeName(), "Point"); + }); + }); + + describe("createModuleTextFromJson", () => { + it("produces valid TypeScript module text for a simple object", () => { + const v = validatorFor(smallSchema, "Point"); + const result = v.createModuleTextFromJson({ x: 1, y: 2 }); + assert.ok(result.success, "expected module text creation to succeed"); + assert.ok(result.data.includes("import { Point } from './schema'")); + assert.ok(result.data.includes("const json: Point =")); + }); + }); +});