diff --git a/.azure-pipelines/typescript-pr-dt.yml b/.azure-pipelines/typescript-pr-dt.yml index 0d9a21d14d..03ec8908d6 100644 --- a/.azure-pipelines/typescript-pr-dt.yml +++ b/.azure-pipelines/typescript-pr-dt.yml @@ -84,6 +84,7 @@ jobs: - task: GoTool@0 displayName: Use Go 1.27 + retryCountOnTaskFailure: 3 inputs: version: "1.27" goDownloadUrl: https://aka.ms/golang/release/latest diff --git a/.changeset/neat-dt-results.md b/.changeset/neat-dt-results.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/neat-dt-results.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/dtslint-runner/src/add-github-links.ts b/packages/dtslint-runner/src/add-github-links.ts index 10219a5dd0..c80ee6eae6 100644 --- a/packages/dtslint-runner/src/add-github-links.ts +++ b/packages/dtslint-runner/src/add-github-links.ts @@ -59,14 +59,16 @@ export async function addGithubLinks(failures: Failure[], definitelyTypedPath: s let stylishFile: string | undefined; for (const match of failure.error.matchAll(/^.*$/gm)) { const line = match[0].replace(/\r$/, ""); - const position = /^(.+?\.(?:[cm]?tsx?|json))(?::(\d+):(\d+)|\((\d+),(\d+)\))/.exec(line); + const prefixLength = line.startsWith("Error: ") ? "Error: ".length : 0; + const position = /^(.+?\.(?:[cm]?tsx?|json))(?::(\d+):(\d+)|\((\d+),(\d+)\))/.exec(line.slice(prefixLength)); const fileHeader = /^(.+\.(?:[cm]?tsx?|json))$/.exec(line); const stylishPosition = /^\s+(\d+):(\d+)\s+(?:error|warning)\b/.exec(line); if (position) { stylishFile = undefined; const file = resolve(packagePath, position[1]); addLocation(file, Number(position[2] ?? position[4]), (url) => { - links.push({ start: match.index, end: match.index + position[0].length, url }); + const start = match.index + prefixLength; + links.push({ start, end: start + position[0].length, url }); }); } else if (fileHeader) { stylishFile = resolve(packagePath, fileHeader[1]); diff --git a/packages/dtslint-runner/src/post-results.ts b/packages/dtslint-runner/src/post-results.ts index e899c68774..000dc00cf9 100644 --- a/packages/dtslint-runner/src/post-results.ts +++ b/packages/dtslint-runner/src/post-results.ts @@ -181,13 +181,14 @@ function getDiffReports(main: Errors, branch: Errors): DiffReport[] { } function formatReport(report: DiffReport, maxErrorLength = Infinity): string { + const packageName = escapeHtml((report.branch ?? report.main)!.path); const errors = [ ...(report.main ? [`Main error:\n${formatProjects(report.main)}${formatError(report.main, maxErrorLength)}`] : []), ...(report.branch ? [`Branch error:\n${formatProjects(report.branch)}${formatError(report.branch, maxErrorLength)}`] : []), ]; - return `
\n${report.title}\n\n${formatPackage((report.branch ?? report.main)!)}\n${errors.join("\n")}\n
`; + return `
\n${report.title} ${packageName}\n\n${formatPackage((report.branch ?? report.main)!)}\n${errors.join("\n")}\n
`; } export function getResultComments(main: Errors, branch: Errors, userToTag: string, logUrl: string): string[] { @@ -302,7 +303,14 @@ function formatError(error: Failure, maxLength = Infinity): string { for (const location of error.errorLinks ?? []) { if (location.end > end) break; result += escapeHtml(error.error.slice(start, location.start)); - result += link(error.error.slice(location.start, location.end), location.url); + const text = error.error.slice(location.start, location.end); + const githubPath = /^https:\/\/github\.com\/DefinitelyTyped\/DefinitelyTyped\/blob\/[^/]+\/([^#]+)(?:#L\d+)?$/.exec( + location.url, + ); + const position = /(?::\d+:\d+|\(\d+,\d+\))$/.exec(text)?.[0] ?? ""; + const label = + githubPath && /\.(?:[cm]?tsx?|json)(?::\d+:\d+|\(\d+,\d+\))?$/.test(text) ? `${githubPath[1]}${position}` : text; + result += link(label, location.url); start = location.end; } result += escapeHtml(error.error.slice(start, end)); diff --git a/packages/dtslint-runner/test/results.test.ts b/packages/dtslint-runner/test/results.test.ts index 7830ebafc6..2518e4d188 100644 --- a/packages/dtslint-runner/test/results.test.ts +++ b/packages/dtslint-runner/test/results.test.ts @@ -84,9 +84,13 @@ test("adds pinned package and Corsa diagnostic links without changing the raw er const comment = getDiffComment([], failures)!; expect(comment).toContain(`example`); expect(comment).toContain( - `
${file}:12:3`,
+    `
types/example/example-tests.ts:12:3`,
   );
   expect(comment).toContain("Type '<T>' is not assignable.");
+  expect(comment).toContain(
+    `types/example/index.d.ts:2:1`,
+  );
+  expect(comment).not.toContain(checkout);
 });
 
 test("links ESLint stylish headers and individual error lines, including CRLF", async () => {
@@ -99,6 +103,26 @@ test("links ESLint stylish headers and individual error lines, including CRLF",
     ["2:3", `${repoUrl}/blob/${commit}/types/example/index.d.ts#L2`],
     ["4:5", `${repoUrl}/blob/${commit}/types/example/index.d.ts#L4`],
   ]);
+  const comment = getDiffComment([], failures)!;
+  expect(comment).toContain(
+    `types/example/index.d.ts`,
+  );
+  expect(comment).toContain(`2:3`);
+});
+
+test("links the first diagnostic after the Error stack prefix", async () => {
+  const file = join(checkout, "types/example/index.d.ts");
+  const error = `Error: ${file}:2:6\nTypeScript@local compile error TS2300:\nDuplicate identifier.\n\n${file}:5:6\nAnother error.`;
+  const failures: Failure[] = [{ path: "example", error }];
+  await addGithubLinks(failures, checkout);
+  expect(failures[0].error).toBe(error);
+  expect(failures[0].errorLinks?.map(({ start, end, url }) => [error.slice(start, end), url])).toEqual([
+    [`${file}:2:6`, `${repoUrl}/blob/${commit}/types/example/index.d.ts#L2`],
+    [`${file}:5:6`, `${repoUrl}/blob/${commit}/types/example/index.d.ts#L5`],
+  ]);
+  expect(getDiffComment([], failures)).toContain(
+    `
Error: types/example/index.d.ts:2:6`,
+  );
 });
 
 test("resolves versioned packages, workspace dependencies, and tsc locations", async () => {
@@ -110,6 +134,13 @@ test("resolves versioned packages, workspace dependencies, and tsc locations", a
   expect(failures[0].packageUrl).toBe(`${repoUrl}/tree/${commit}/types/example/v1`);
   expect(failures[0].errorLinks?.[0].url).toBe(`${repoUrl}/blob/${commit}/types/example/v1/index.d.ts#L3`);
   expect(failures[1].errorLinks?.[0].url).toBe(`${repoUrl}/blob/${commit}/types/dependency/index.d.ts#L5`);
+  const comment = getDiffComment([], failures)!;
+  expect(comment).toContain(
+    `types/example/v1/index.d.ts(3,4)`,
+  );
+  expect(comment).toContain(
+    `types/dependency/index.d.ts:5:6`,
+  );
 });
 
 test("leaves untracked, missing, installed, and external files unlinked", async () => {
@@ -166,9 +197,9 @@ test("renders all three difference categories, retaining each side's links", ()
       { path: "changed", error: "new", errorLinks: [{ start: 0, end: 3, url: branchUrl }] },
     ],
   )!;
-  expect(comment).toContain("Branch only errors:");
-  expect(comment).toContain("Main only errors:");
-  expect(comment).toContain("Errors that changed between main and the branch:");
+  expect(comment).toContain("Branch only errors: new");
+  expect(comment).toContain("Main only errors: fixed");
+  expect(comment).toContain("Errors that changed between main and the branch: changed");
   expect(comment).toContain(`
old
`); expect(comment).toContain(`
new
`); expect(comment).toContain("
old failure
"); @@ -188,6 +219,7 @@ test("escapes diagnostic HTML and refuses non-DT links", () => { }, ], )!; + expect(comment).toContain("Branch only errors: <example>"); expect(comment).toContain("Package: <example>"); expect(comment).toContain("</pre><script>alert("oops")</script>\n```\n& <T>"); expect(comment).not.toContain("