|
| 1 | +import * as ts from 'typescript'; |
| 2 | +import * as minimatch from 'minimatch'; |
| 3 | +import * as path from 'path'; |
| 4 | +import { IncrementalCheckerInterface } from './IncrementalCheckerInterface'; |
| 5 | +import { CancellationToken } from './CancellationToken'; |
| 6 | +import { NormalizedMessage } from './NormalizedMessage'; |
| 7 | +import { Configuration, Linter, LintResult } from 'tslint'; |
| 8 | +import { CompilerHost } from './CompilerHost'; |
| 9 | +import { FsHelper } from './FsHelper'; |
| 10 | + |
| 11 | +// Need some augmentation here - linterOptions.exclude is not (yet) part of the official |
| 12 | +// types for tslint. |
| 13 | +interface ConfigurationFile extends Configuration.IConfigurationFile { |
| 14 | + linterOptions?: { |
| 15 | + typeCheck?: boolean; |
| 16 | + exclude?: string[]; |
| 17 | + }; |
| 18 | +} |
| 19 | + |
| 20 | +export class ApiIncrementalChecker implements IncrementalCheckerInterface { |
| 21 | + private linterConfig?: ConfigurationFile; |
| 22 | + |
| 23 | + private readonly tsIncrementalCompiler: CompilerHost; |
| 24 | + private linterExclusions: minimatch.IMinimatch[] = []; |
| 25 | + |
| 26 | + private currentLintErrors = new Map<string, LintResult>(); |
| 27 | + private lastUpdatedFiles: string[] = []; |
| 28 | + private lastRemovedFiles: string[] = []; |
| 29 | + |
| 30 | + constructor( |
| 31 | + programConfigFile: string, |
| 32 | + compilerOptions: ts.CompilerOptions, |
| 33 | + private linterConfigFile: string | false, |
| 34 | + private linterAutoFix: boolean, |
| 35 | + checkSyntacticErrors: boolean |
| 36 | + ) { |
| 37 | + this.initLinterConfig(); |
| 38 | + |
| 39 | + this.tsIncrementalCompiler = new CompilerHost( |
| 40 | + programConfigFile, |
| 41 | + compilerOptions, |
| 42 | + checkSyntacticErrors |
| 43 | + ); |
| 44 | + } |
| 45 | + |
| 46 | + private initLinterConfig() { |
| 47 | + if (!this.linterConfig && this.linterConfigFile) { |
| 48 | + this.linterConfig = ApiIncrementalChecker.loadLinterConfig( |
| 49 | + this.linterConfigFile |
| 50 | + ); |
| 51 | + |
| 52 | + if ( |
| 53 | + this.linterConfig.linterOptions && |
| 54 | + this.linterConfig.linterOptions.exclude |
| 55 | + ) { |
| 56 | + // Pre-build minimatch patterns to avoid additional overhead later on. |
| 57 | + // Note: Resolving the path is required to properly match against the full file paths, |
| 58 | + // and also deals with potential cross-platform problems regarding path separators. |
| 59 | + this.linterExclusions = this.linterConfig.linterOptions.exclude.map( |
| 60 | + pattern => new minimatch.Minimatch(path.resolve(pattern)) |
| 61 | + ); |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + private static loadLinterConfig(configFile: string): ConfigurationFile { |
| 67 | + const tslint = require('tslint'); |
| 68 | + |
| 69 | + return tslint.Configuration.loadConfigurationFromPath( |
| 70 | + configFile |
| 71 | + ) as ConfigurationFile; |
| 72 | + } |
| 73 | + |
| 74 | + private createLinter(program: ts.Program): Linter { |
| 75 | + const tslint = require('tslint'); |
| 76 | + |
| 77 | + return new tslint.Linter({ fix: this.linterAutoFix }, program); |
| 78 | + } |
| 79 | + |
| 80 | + public hasLinter(): boolean { |
| 81 | + return !!this.linterConfig; |
| 82 | + } |
| 83 | + |
| 84 | + public isFileExcluded(filePath: string): boolean { |
| 85 | + return ( |
| 86 | + filePath.endsWith('.d.ts') || |
| 87 | + this.linterExclusions.some(matcher => matcher.match(filePath)) |
| 88 | + ); |
| 89 | + } |
| 90 | + |
| 91 | + public nextIteration() { |
| 92 | + // do nothing |
| 93 | + } |
| 94 | + |
| 95 | + public async getDiagnostics(_cancellationToken: CancellationToken) { |
| 96 | + const diagnostics = await this.tsIncrementalCompiler.processChanges(); |
| 97 | + this.lastUpdatedFiles = diagnostics.updatedFiles; |
| 98 | + this.lastRemovedFiles = diagnostics.removedFiles; |
| 99 | + |
| 100 | + return NormalizedMessage.deduplicate( |
| 101 | + diagnostics.results.map(NormalizedMessage.createFromDiagnostic) |
| 102 | + ); |
| 103 | + } |
| 104 | + |
| 105 | + public getLints(_cancellationToken: CancellationToken) { |
| 106 | + if (!this.linterConfig) { |
| 107 | + return []; |
| 108 | + } |
| 109 | + |
| 110 | + for (const updatedFile of this.lastUpdatedFiles) { |
| 111 | + if (this.isFileExcluded(updatedFile)) { |
| 112 | + continue; |
| 113 | + } |
| 114 | + |
| 115 | + try { |
| 116 | + const linter = this.createLinter( |
| 117 | + this.tsIncrementalCompiler.getProgram() |
| 118 | + ); |
| 119 | + // const source = fs.readFileSync(updatedFile, 'utf-8'); |
| 120 | + linter.lint(updatedFile, undefined!, this.linterConfig); |
| 121 | + const lints = linter.getResult(); |
| 122 | + this.currentLintErrors.set(updatedFile, lints); |
| 123 | + } catch (e) { |
| 124 | + if ( |
| 125 | + FsHelper.existsSync(updatedFile) && |
| 126 | + // check the error type due to file system lag |
| 127 | + !(e instanceof Error) && |
| 128 | + !(e.constructor.name === 'FatalError') && |
| 129 | + !(e.message && e.message.trim().startsWith('Invalid source file')) |
| 130 | + ) { |
| 131 | + // it's not because file doesn't exist - throw error |
| 132 | + throw e; |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + for (const removedFile of this.lastRemovedFiles) { |
| 137 | + this.currentLintErrors.delete(removedFile); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + const allLints = []; |
| 142 | + for (const [, value] of this.currentLintErrors) { |
| 143 | + allLints.push(...value.failures); |
| 144 | + } |
| 145 | + |
| 146 | + return NormalizedMessage.deduplicate( |
| 147 | + allLints.map(NormalizedMessage.createFromLint) |
| 148 | + ); |
| 149 | + } |
| 150 | +} |
0 commit comments