Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.

Commit 2819165

Browse files
committed
Suggest the required form
1 parent 34671d3 commit 2819165

File tree

5 files changed

+106
-23
lines changed

5 files changed

+106
-23
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"idembot": "^0.0.12",
2626
"moment": "^2.27.0",
2727
"node-fetch": "^1.7.3",
28+
"prettier": "^2.2.0",
2829
"prettyjson": "^1.2.1",
2930
"request": "^2.88.2",
3031
"tslib": "^1.13.0",

src/comments.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,8 @@ I can't [accept a merge request](${uri}) until the PR has a green CI and was app
9191
9292
Thanks, and happy typing!`
9393
});
94+
95+
export const Suggestions = (user: string) => ({
96+
tag: "suggestions",
97+
status: `@${user} I noticed these differences from the required form. If you can revise your changes to avoid them, so much the better! Otherwise please reply with explanations why they're needed (unless it's obvious) and a maintainer will take a look. Thanks!`,
98+
});

src/compute-pr-actions.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Comments from "./comments";
22
import { PrInfo, BotError, BotEnsureRemovedFromProject, BotNoPackages, FileInfo } from "./pr-info";
33
import { CIResult } from "./util/CIResult";
4-
import { ReviewInfo } from "./pr-info";
4+
import { ReviewInfo, Suggestion } from "./pr-info";
55
import { noNulls, flatten, unique, sameUser, daysSince, sha256 } from "./util/util";
66

77
type ColumnName =
@@ -44,6 +44,7 @@ export interface Actions {
4444
targetColumn?: ColumnName;
4545
labels: LabelName[];
4646
responseComments: Comments.Comment[];
47+
suggestions: { [path: string]: Suggestion };
4748
shouldClose: boolean;
4849
shouldMerge: boolean;
4950
shouldUpdateLabels: boolean;
@@ -58,6 +59,7 @@ function createDefaultActions(pr_number: number): Actions {
5859
targetColumn: "Other",
5960
labels: [],
6061
responseComments: [],
62+
suggestions: {},
6163
shouldClose: false,
6264
shouldMerge: false,
6365
shouldUpdateLabels: true,
@@ -72,6 +74,7 @@ function createEmptyActions(prNumber: number): Actions {
7274
pr_number: prNumber,
7375
labels: [],
7476
responseComments: [],
77+
suggestions: {},
7578
shouldClose: false,
7679
shouldMerge: false,
7780
shouldUpdateLabels: false,
@@ -305,6 +308,15 @@ export function process(prInfo: PrInfo | BotEnsureRemovedFromProject | BotNoPack
305308
// Update intro comment
306309
post({ tag: "welcome", status: createWelcomeComment(info) });
307310

311+
// Propagate suggestions into actions
312+
for (const pkg of info.pkgInfo) {
313+
for (const file of pkg.files) {
314+
if (file.suggestion) {
315+
context.suggestions[file.path] = file.suggestion;
316+
}
317+
}
318+
}
319+
308320
// Ping reviewers when needed
309321
if (!(info.hasChangereqs || info.approvedBy.includes("owner") || info.approvedBy.includes("maintainer"))) {
310322
if (info.noOtherOwners) {

src/execute-pr-actions.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createMutation, mutate } from "./graphql-client";
44
import { getProjectBoardColumns, getLabels } from "./util/cachedQueries";
55
import { noNulls, flatten } from "./util/util";
66
import * as comment from "./util/comment";
7+
import * as comments from "./comments";
78

89
// https://github.com/DefinitelyTyped/DefinitelyTyped/projects/5
910
const ProjectBoardNumber = 5;
@@ -19,6 +20,9 @@ const addProjectCard = `mutation($input: AddProjectCardInput!) { addProjectCard(
1920
const moveProjectCard = `mutation($input: MoveProjectCardInput!) { moveProjectCard(input: $input) { clientMutationId } }`;
2021
export const deleteProjectCard = `mutation($input: DeleteProjectCardInput!) { deleteProjectCard(input: $input) { clientMutationId } }`;
2122

23+
const addPullRequestReviewThread = 'mutation($input: AddPullRequestReviewThreadInput!) { addPullRequestReviewThread(input: $input) { clientMutationId } }';
24+
const submitPullRequestReview = 'mutation($input: SubmitPullRequestReviewInput!) { submitPullRequestReview(input: $input) { clientMutationId } }';
25+
2226
export async function executePrActions(actions: Actions, info: PRQueryResult, dry?: boolean) {
2327
const pr = info.repository?.pullRequest!;
2428
const botComments: ParsedComment[] = getBotComments(pr);
@@ -27,6 +31,7 @@ export async function executePrActions(actions: Actions, info: PRQueryResult, dr
2731
...await getMutationsForProjectChanges(actions, pr),
2832
...getMutationsForComments(actions, pr.id, botComments),
2933
...getMutationsForCommentRemovals(actions, botComments),
34+
...getMutationsForSuggestions(actions, pr),
3035
...getMutationsForChangingPRState(actions, pr),
3136
]);
3237
if (!dry) {
@@ -106,6 +111,32 @@ function getMutationsForCommentRemovals(actions: Actions, botComments: ParsedCom
106111
});
107112
}
108113

114+
function getMutationsForSuggestions(actions: Actions, pr: PR_repository_pullRequest) {
115+
if (pr.reviews?.nodes?.find(review => review?.author?.login === "typescript-bot")) return [];
116+
const mutations = Object.entries(actions.suggestions).map(([path, { startLine, line, suggestion }]) =>
117+
createMutation(addPullRequestReviewThread, {
118+
input: {
119+
pullRequestId: pr.id,
120+
path,
121+
startLine: startLine === line ? undefined : startLine,
122+
line,
123+
body: "```suggestion\n" + suggestion,
124+
}
125+
})
126+
);
127+
if (mutations.length === 0) return [];
128+
return [
129+
...mutations,
130+
createMutation(submitPullRequestReview, {
131+
input: {
132+
pullRequestId: pr.id,
133+
body: comment.make(comments.Suggestions(pr.author!.login)),
134+
event: "COMMENT",
135+
}
136+
}),
137+
];
138+
}
139+
109140
function getMutationsForChangingPRState(actions: Actions, pr: PR_repository_pullRequest) {
110141
return [
111142
actions.shouldMerge

src/pr-info.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { noNulls, notUndefined, findLast, forEachReverse, sameUser, authorNotBot
2020
import * as comment from "./util/comment";
2121
import * as HeaderParser from "definitelytyped-header-parser";
2222
import * as jsonDiff from "fast-json-patch";
23+
import * as prettier from "prettier";
2324
import { PullRequestState } from "./schema/graphql-global-types";
2425

2526
const CriticalPopularityThreshold = 5_000_000;
@@ -71,9 +72,16 @@ type FileKind = "test" | "definition" | "markdown" | "package-meta" | "package-m
7172
export type FileInfo = {
7273
path: string,
7374
kind: FileKind,
74-
suspect?: string // reason for a file being "package-meta" rather than "package-meta-ok"
75+
suspect?: string, // reason for a file being "package-meta" rather than "package-meta-ok"
76+
suggestion?: Suggestion,
7577
};
7678

79+
export interface Suggestion {
80+
readonly startLine: number;
81+
readonly line: number;
82+
readonly suggestion: string;
83+
}
84+
7785
export type ReviewInfo = {
7886
type: string,
7987
reviewer: string,
@@ -348,21 +356,21 @@ async function categorizeFile(path: string, contents: (oid?: string) => Promise<
348356
case "md": return [pkg, { path, kind: "markdown" }];
349357
default:
350358
const suspect = await configSuspicious(path, contents);
351-
return [pkg, { path, kind: suspect ? "package-meta" : "package-meta-ok", suspect }];
359+
return [pkg, { path, kind: suspect ? "package-meta" : "package-meta-ok", ...suspect }];
352360
}
353361
}
354362

355363
interface ConfigSuspicious {
356-
(path: string, getContents: (oid?: string) => Promise<string | undefined>): Promise<string | undefined>;
357-
[basename: string]: (text: string, oldText?: string) => string | undefined;
364+
(path: string, getContents: (oid?: string) => Promise<string | undefined>): Promise<{ suspect: string, sugestion?: Suggestion } | undefined>;
365+
[basename: string]: (text: string, oldText?: string) => { suspect: string, suggestion?: Suggestion } | undefined;
358366
};
359367
const configSuspicious = <ConfigSuspicious>(async (path, getContents) => {
360368
const basename = path.replace(/.*\//, "");
361-
if (!(basename in configSuspicious)) return `edited`;
369+
if (!(basename in configSuspicious)) return { suspect: `edited` };
362370
const text = await getContents();
363-
if (text === undefined) return `couldn't fetch contents`;
371+
if (text === undefined) return { suspect: `couldn't fetch contents` };
364372
const tester = configSuspicious[basename];
365-
let suspect: string | undefined;
373+
let suspect;
366374
if (tester.length === 1) {
367375
suspect = tester(text);
368376
} else {
@@ -373,7 +381,7 @@ const configSuspicious = <ConfigSuspicious>(async (path, getContents) => {
373381
});
374382
configSuspicious["OTHER_FILES.txt"] = contents =>
375383
// not empty
376-
(contents.length === 0) ? "empty"
384+
(contents.length === 0) ? { suspect: "empty" }
377385
: undefined;
378386
configSuspicious["package.json"] = makeJsonCheckerFromCore(
379387
{ private: true },
@@ -404,22 +412,48 @@ configSuspicious["tsconfig.json"] = makeJsonCheckerFromCore(
404412
// to it, ignoring some keys (JSON Patch paths). The ignored properties are in most cases checked
405413
// elsewhere (dtslint), and in some cases they are irrelevant.
406414
function makeJsonCheckerFromCore(requiredForm: any, ignoredKeys: string[]) {
407-
const diffFromReq = (text: string) => {
408-
let json: any;
409-
try { json = JSON.parse(text); } catch (e) { return "couldn't parse json"; }
410-
jsonDiff.applyPatch(json, ignoredKeys.map(path => ({ op: "remove", path })));
411-
try { return jsonDiff.compare(requiredForm, json); } catch (e) { return "couldn't diff json" };
412-
};
413415
return (contents: string, oldText?: string) => {
414-
const newDiff = diffFromReq(contents);
415-
if (typeof newDiff === "string") return newDiff;
416+
const diffFromReq = (json: any) => {
417+
jsonDiff.applyPatch(json, ignoredKeys.map(path => ({ op: "remove", path })));
418+
return jsonDiff.compare(requiredForm, json);
419+
};
420+
let newJson;
421+
try { newJson = JSON.parse(contents); } catch (e) { return { suspect: "couldn't parse json" }; }
422+
const suggestion = jsonDiff.deepClone(newJson);
423+
const newDiff = diffFromReq(newJson);
416424
if (newDiff.length === 0) return undefined;
417-
if (!oldText) return `not the required form \`${JSON.stringify(newDiff)}\``;
418-
const oldDiff = diffFromReq(oldText);
419-
if (typeof oldDiff === "string") return oldDiff;
420-
const notRemove = jsonDiff.compare(oldDiff, newDiff).filter(({ op }) => op !== "remove");
421-
if (notRemove.length === 0) return undefined;
422-
return `not the required form and not moving towards it \`${JSON.stringify(notRemove.map(({ path }) => newDiff[Number(path.slice(1))]))}\``;
425+
const towardsIt = jsonDiff.deepClone(requiredForm);
426+
if (oldText) {
427+
let oldJson;
428+
try { oldJson = JSON.parse(oldText); } catch (e) { return { suspect: "couldn't parse json" }; }
429+
const oldDiff = diffFromReq(oldJson);
430+
const notRemove = jsonDiff.compare(oldDiff, newDiff).filter(({ op }) => op !== "remove");
431+
if (notRemove.length === 0) return undefined;
432+
jsonDiff.applyPatch(newDiff, notRemove.map(({ path }) => ({ op: "remove", path })));
433+
jsonDiff.applyPatch(towardsIt, newDiff.filter(({ op }) => op));
434+
}
435+
jsonDiff.applyPatch(suggestion, jsonDiff.compare(newJson, towardsIt));
436+
// Suggest the different lines to the author
437+
const suggestionLines = (
438+
Object.keys(suggestion).length <= 1
439+
? prettier.format(JSON.stringify(suggestion), { tabWidth: 4, filepath: ".json" })
440+
: JSON.stringify(suggestion, undefined, 4) + "\n"
441+
).split(/^/m);
442+
const lines = contents.split(/^/m);
443+
let i = 0;
444+
while (suggestionLines[i].trim() === lines[i].trim()) i++;
445+
let j = 1;
446+
while (suggestionLines[suggestionLines.length - j].trim() === lines[lines.length - j].trim()) j++;
447+
return {
448+
suspect: oldText
449+
? "not the required form and not moving towards it"
450+
: "not the required form",
451+
suggestion: {
452+
startLine: i + 1,
453+
line: lines.length - j + 1,
454+
suggestion: suggestionLines.slice(i, 1 - j || undefined).join(""),
455+
},
456+
};
423457
};
424458
}
425459

0 commit comments

Comments
 (0)