@@ -20,6 +20,7 @@ import { noNulls, notUndefined, findLast, forEachReverse, sameUser, authorNotBot
2020import * as comment from "./util/comment" ;
2121import * as HeaderParser from "definitelytyped-header-parser" ;
2222import * as jsonDiff from "fast-json-patch" ;
23+ import * as prettier from "prettier" ;
2324import { PullRequestState } from "./schema/graphql-global-types" ;
2425
2526const CriticalPopularityThreshold = 5_000_000 ;
@@ -71,9 +72,16 @@ type FileKind = "test" | "definition" | "markdown" | "package-meta" | "package-m
7172export 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+
7785export 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
355363interface 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} ;
359367const 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} ) ;
374382configSuspicious [ "OTHER_FILES.txt" ] = contents =>
375383 // not empty
376- ( contents . length === 0 ) ? "empty"
384+ ( contents . length === 0 ) ? { suspect : "empty" }
377385 : undefined ;
378386configSuspicious [ "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.
406414function 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