@@ -20,6 +20,7 @@ import { noNulls, notUndefined, findLast, forEachReverse, sameUser, authorNotBot
20
20
import * as comment from "./util/comment" ;
21
21
import * as HeaderParser from "definitelytyped-header-parser" ;
22
22
import * as jsonDiff from "fast-json-patch" ;
23
+ import * as prettier from "prettier" ;
23
24
import { PullRequestState } from "./schema/graphql-global-types" ;
24
25
25
26
const CriticalPopularityThreshold = 5_000_000 ;
@@ -71,9 +72,16 @@ type FileKind = "test" | "definition" | "markdown" | "package-meta" | "package-m
71
72
export type FileInfo = {
72
73
path : string ,
73
74
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 ,
75
77
} ;
76
78
79
+ export interface Suggestion {
80
+ readonly startLine : number ;
81
+ readonly line : number ;
82
+ readonly suggestion : string ;
83
+ }
84
+
77
85
export type ReviewInfo = {
78
86
type : string ,
79
87
reviewer : string ,
@@ -348,21 +356,21 @@ async function categorizeFile(path: string, contents: (oid?: string) => Promise<
348
356
case "md" : return [ pkg , { path, kind : "markdown" } ] ;
349
357
default :
350
358
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 } ] ;
352
360
}
353
361
}
354
362
355
363
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 ;
358
366
} ;
359
367
const configSuspicious = < ConfigSuspicious > ( async ( path , getContents ) => {
360
368
const basename = path . replace ( / .* \/ / , "" ) ;
361
- if ( ! ( basename in configSuspicious ) ) return `edited` ;
369
+ if ( ! ( basename in configSuspicious ) ) return { suspect : `edited` } ;
362
370
const text = await getContents ( ) ;
363
- if ( text === undefined ) return `couldn't fetch contents` ;
371
+ if ( text === undefined ) return { suspect : `couldn't fetch contents` } ;
364
372
const tester = configSuspicious [ basename ] ;
365
- let suspect : string | undefined ;
373
+ let suspect ;
366
374
if ( tester . length === 1 ) {
367
375
suspect = tester ( text ) ;
368
376
} else {
@@ -373,7 +381,7 @@ const configSuspicious = <ConfigSuspicious>(async (path, getContents) => {
373
381
} ) ;
374
382
configSuspicious [ "OTHER_FILES.txt" ] = contents =>
375
383
// not empty
376
- ( contents . length === 0 ) ? "empty"
384
+ ( contents . length === 0 ) ? { suspect : "empty" }
377
385
: undefined ;
378
386
configSuspicious [ "package.json" ] = makeJsonCheckerFromCore (
379
387
{ private : true } ,
@@ -404,22 +412,48 @@ configSuspicious["tsconfig.json"] = makeJsonCheckerFromCore(
404
412
// to it, ignoring some keys (JSON Patch paths). The ignored properties are in most cases checked
405
413
// elsewhere (dtslint), and in some cases they are irrelevant.
406
414
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
- } ;
413
415
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 ) ;
416
424
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
+ } ;
423
457
} ;
424
458
}
425
459
0 commit comments