import * as ts from 'typescript' ; /**
 * A Tsetse check Failure is almost identical to a Diagnostic from TypeScript
 * except that:
 * (1) The error code is defined by each individual Tsetse rule.
 * (2) The optional `source` property is set to `Tsetse` so the host (VS Code
 * for instance) would use that to indicate where the error comes from.
 * (3) There's an optional suggestedFixes field.
 */ export class Failure { constructor ( private readonly sourceFile , private readonly start , private readonly end , private readonly failureText , private readonly code , /**
     * The origin of the failure, e.g., the name of the rule reporting the
     * failure. Can be empty.
     */ private readonly failureSource , private readonly suggestedFixes = [ ] ) { } /**
     * This returns a structure compatible with ts.Diagnostic, but with added
     * fields, for convenience and to support suggested fixes.
     */ toDiagnostic ( ) { return { file : this . sourceFile , start : this . start , end : this . end , length : this . end - this . start , messageText : this . failureSource ? `[${ this . failureSource }] ${ this . failureText }` : this . failureText , category : ts . DiagnosticCategory . Error , code : this . code , source : this . failureSource , fixes : this . suggestedFixes } ; } /**
     * Same as toDiagnostic, but include the fix in the message, so that systems
     * that don't support displaying suggested fixes can still surface that
     * information. This assumes the diagnostic message is going to be presented
     * within the context of the problematic code.
     */ toDiagnosticWithStringifiedFixes ( ) { const diagnostic = this . toDiagnostic ( ) ; if ( this . suggestedFixes . length ) { diagnostic . messageText += ' ' + this . mapFixesToReadableString ( ) ; } return diagnostic ; } toString ( ) { return `{ sourceFile:${ this . sourceFile ? this . sourceFile . fileName : 'unknown' }, start:${ this . start }, end:${ this . end }, source:${ this . failureSource }, fixes:${ JSON . stringify ( this . suggestedFixes . map ( fix => fixToString ( fix ) ) ) } }` ; } /***
     * Stringifies an array of `suggestedFixes` for this failure. This is just a
     * heuristic and should be used in systems which do not support fixers
     * integration (e.g. CLI tools).
     */ mapFixesToReadableString ( ) { const stringifiedFixes = this . suggestedFixes . map ( fix => this . fixToReadableString ( fix ) ) . join ( '\nOR\n' ) ; if ( ! stringifiedFixes ) return '' ; else if ( this . suggestedFixes . length === 1 ) { return 'Suggested fix:\n' + stringifiedFixes ; } else { return 'Suggested fixes:\n' + stringifiedFixes ; } } /**
     * Stringifies a `Fix`, in a way that makes sense when presented alongside the
     * finding. This is a heuristic, obviously.
     */ fixToReadableString ( f ) { let fixText = '' ; for ( const c of f . changes ) { const printableReplacement = c . replacement . trim ( ) ; if ( c . start === c . end ) { if ( c . replacement . indexOf ( 'import' ) !== - 1 ) { fixText += `- Add new import: ${ printableReplacement }\n` ; } else { fixText += `- Insert ${ this . readableRange ( c . start , c . end ) }: ${ printableReplacement }\n` ; } } else if ( c . start === this . start && c . end === this . end ) { if ( printableReplacement === '' ) { fixText = `- Delete the full match\n` + fixText ; } else { fixText = `- Replace the full match with: ${ printableReplacement }\n` + fixText ; } } else { if ( printableReplacement === '' ) { fixText = `- Delete ${ this . readableRange ( c . start , c . end ) }\n` + fixText ; } else { fixText = `- Replace ${ this . readableRange ( c . start , c . end ) } with: ` + `${ printableReplacement }\n${ fixText }` ; } } } return fixText . trim ( ) ; } /**
     * Turns the range to a human readable format to be used by fixers.
     *
     * If the length of the range is non zero it returns the source file text
     * representing the range. Otherwise returns the stringified representation of
     * the source file position.
     */ readableRange ( from , to ) { const lcf = this . sourceFile . getLineAndCharacterOfPosition ( from ) ; const lct = this . sourceFile . getLineAndCharacterOfPosition ( to ) ; if ( lcf . line === lct . line && lcf . character === lct . character ) { return `at line ${ lcf . line + 1 }, char ${ lcf . character + 1 }` ; } else { return `'${ this . sourceFile . text . substring ( from , to ) . replace ( /\n/g , '\\n' ) }'` ; } } } /**
 * A `Fix` is a potential repair to the associated `Failure`.
 */ export interface Fix { /**
     * The individual text replacements composing that fix.
     */ changes : IndividualChange [ ] ; } /** Creates a fix that replaces the given node with the new text. */ export function replaceNode ( node , replacement ) { return { changes : [ { sourceFile : node . getSourceFile ( ) , start : node . getStart ( ) , end : node . getEnd ( ) , replacement , } ] , } ; } /**
 * An individual text replacement/insertion in a source file. Used as part of a
 * `Fix`.
 */ export interface IndividualChange { sourceFile : ts . SourceFile ; start : number ; end : number ; replacement : string ; } /**
 * A ts.Diagnostic that might include fixes, and with an added `end`
 * field for convenience.
 */ export interface DiagnosticWithFixes extends ts . Diagnostic { end : number ; /**
     * An array of fixes for a given diagnostic.
     *
     * Each element (fix) of the array provides a different alternative on how to
     * fix the diagnostic. Every fix is self contained and indepedent to other
     * fixes in the array.
     *
     * These fixes can be integrated into IDEs and presented to the users who can
     * choose the most suitable fix.
     */ fixes : Fix [ ] ; } /**
 * Stringifies a `Fix`, replacing the `ts.SourceFile` with the matching
 * filename.
 */ export function fixToString ( f ? ) { if ( ! f ) return 'undefined' ; return '{' + JSON . stringify ( f . changes . map ( ic => { return { start : ic . start , end : ic . end , replacement : ic . replacement , fileName : ic . sourceFile . fileName } ; } ) ) + '}' ; }