|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google Inc. All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | +import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect/src/index2'; |
| 9 | +import { json } from '@angular-devkit/core'; |
| 10 | +import { readFileSync } from 'fs'; |
| 11 | +import * as glob from 'glob'; |
| 12 | +import { Minimatch } from 'minimatch'; |
| 13 | +import * as path from 'path'; |
| 14 | +import * as tslint from 'tslint'; // tslint:disable-line:no-implicit-dependencies |
| 15 | +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies |
| 16 | +import { stripBom } from '../angular-cli-files/utilities/strip-bom'; |
| 17 | +import { Schema as RealTslintBuilderOptions } from './schema'; |
| 18 | + |
| 19 | + |
| 20 | +type TslintBuilderOptions = RealTslintBuilderOptions & json.JsonObject; |
| 21 | + |
| 22 | + |
| 23 | +async function _loadTslint() { |
| 24 | + let tslint; |
| 25 | + try { |
| 26 | + tslint = await import('tslint'); // tslint:disable-line:no-implicit-dependencies |
| 27 | + } catch { |
| 28 | + throw new Error('Unable to find TSLint. Ensure TSLint is installed.'); |
| 29 | + } |
| 30 | + |
| 31 | + const version = tslint.Linter.VERSION && tslint.Linter.VERSION.split('.'); |
| 32 | + if (!version || version.length < 2 || Number(version[0]) < 5 || Number(version[1]) < 5) { |
| 33 | + throw new Error('TSLint must be version 5.5 or higher.'); |
| 34 | + } |
| 35 | + |
| 36 | + return tslint; |
| 37 | +} |
| 38 | + |
| 39 | + |
| 40 | +async function _run(config: TslintBuilderOptions, context: BuilderContext): Promise<BuilderOutput> { |
| 41 | + const systemRoot = context.workspaceRoot; |
| 42 | + process.chdir(context.currentDirectory); |
| 43 | + const options = config; |
| 44 | + const projectName = context.target && context.target.project || '<???>'; |
| 45 | + |
| 46 | + // Print formatter output only for non human-readable formats. |
| 47 | + const printInfo = ['prose', 'verbose', 'stylish'].includes(options.format || '') |
| 48 | + && !options.silent; |
| 49 | + |
| 50 | + context.reportStatus(`Linting ${JSON.stringify(projectName)}...`); |
| 51 | + if (printInfo) { |
| 52 | + context.logger.info(`Linting ${JSON.stringify(projectName)}...`); |
| 53 | + } |
| 54 | + |
| 55 | + if (!options.tsConfig && options.typeCheck) { |
| 56 | + throw new Error('A "project" must be specified to enable type checking.'); |
| 57 | + } |
| 58 | + |
| 59 | + const projectTslint = await _loadTslint(); |
| 60 | + const tslintConfigPath = options.tslintConfig |
| 61 | + ? path.resolve(systemRoot, options.tslintConfig) |
| 62 | + : null; |
| 63 | + const Linter = projectTslint.Linter; |
| 64 | + |
| 65 | + let result: undefined | tslint.LintResult = undefined; |
| 66 | + if (options.tsConfig) { |
| 67 | + const tsConfigs = Array.isArray(options.tsConfig) ? options.tsConfig : [options.tsConfig]; |
| 68 | + context.reportProgress(0, tsConfigs.length); |
| 69 | + const allPrograms = tsConfigs.map(tsConfig => { |
| 70 | + return Linter.createProgram(path.resolve(systemRoot, tsConfig)); |
| 71 | + }); |
| 72 | + |
| 73 | + let i = 0; |
| 74 | + for (const program of allPrograms) { |
| 75 | + const partial |
| 76 | + = await _lint(projectTslint, systemRoot, tslintConfigPath, options, program, allPrograms); |
| 77 | + if (result === undefined) { |
| 78 | + result = partial; |
| 79 | + } else { |
| 80 | + result.failures = result.failures |
| 81 | + .filter(curr => { |
| 82 | + return !partial.failures.some(prev => curr.equals(prev)); |
| 83 | + }) |
| 84 | + .concat(partial.failures); |
| 85 | + |
| 86 | + // we are not doing much with 'errorCount' and 'warningCount' |
| 87 | + // apart from checking if they are greater than 0 thus no need to dedupe these. |
| 88 | + result.errorCount += partial.errorCount; |
| 89 | + result.warningCount += partial.warningCount; |
| 90 | + |
| 91 | + if (partial.fixes) { |
| 92 | + result.fixes = result.fixes ? result.fixes.concat(partial.fixes) : partial.fixes; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + context.reportProgress(++i, allPrograms.length); |
| 97 | + } |
| 98 | + } else { |
| 99 | + result = await _lint(projectTslint, systemRoot, tslintConfigPath, options); |
| 100 | + } |
| 101 | + |
| 102 | + if (result == undefined) { |
| 103 | + throw new Error('Invalid lint configuration. Nothing to lint.'); |
| 104 | + } |
| 105 | + |
| 106 | + if (!options.silent) { |
| 107 | + const Formatter = projectTslint.findFormatter(options.format || ''); |
| 108 | + if (!Formatter) { |
| 109 | + throw new Error(`Invalid lint format "${options.format}".`); |
| 110 | + } |
| 111 | + const formatter = new Formatter(); |
| 112 | + |
| 113 | + const output = formatter.format(result.failures, result.fixes); |
| 114 | + if (output.trim()) { |
| 115 | + context.logger.info(output); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + if (result.warningCount > 0 && printInfo) { |
| 120 | + context.logger.warn('Lint warnings found in the listed files.'); |
| 121 | + } |
| 122 | + |
| 123 | + if (result.errorCount > 0 && printInfo) { |
| 124 | + context.logger.error('Lint errors found in the listed files.'); |
| 125 | + } |
| 126 | + |
| 127 | + if (result.warningCount === 0 && result.errorCount === 0 && printInfo) { |
| 128 | + context.logger.info('All files pass linting.'); |
| 129 | + } |
| 130 | + |
| 131 | + return { |
| 132 | + success: options.force || result.errorCount === 0, |
| 133 | + }; |
| 134 | +} |
| 135 | + |
| 136 | + |
| 137 | +export default createBuilder<TslintBuilderOptions>(_run); |
| 138 | + |
| 139 | + |
| 140 | +async function _lint( |
| 141 | + projectTslint: typeof tslint, |
| 142 | + systemRoot: string, |
| 143 | + tslintConfigPath: string | null, |
| 144 | + options: TslintBuilderOptions, |
| 145 | + program?: ts.Program, |
| 146 | + allPrograms?: ts.Program[], |
| 147 | +) { |
| 148 | + const Linter = projectTslint.Linter; |
| 149 | + const Configuration = projectTslint.Configuration; |
| 150 | + |
| 151 | + const files = getFilesToLint(systemRoot, options, Linter, program); |
| 152 | + const lintOptions = { |
| 153 | + fix: !!options.fix, |
| 154 | + formatter: options.format, |
| 155 | + }; |
| 156 | + |
| 157 | + const linter = new Linter(lintOptions, program); |
| 158 | + |
| 159 | + let lastDirectory: string | undefined = undefined; |
| 160 | + let configLoad; |
| 161 | + for (const file of files) { |
| 162 | + if (program && allPrograms) { |
| 163 | + // If it cannot be found in ANY program, then this is an error. |
| 164 | + if (allPrograms.every(p => p.getSourceFile(file) === undefined)) { |
| 165 | + throw new Error( |
| 166 | + `File ${JSON.stringify(file)} is not part of a TypeScript project '${options.tsConfig}'.`, |
| 167 | + ); |
| 168 | + } else if (program.getSourceFile(file) === undefined) { |
| 169 | + // The file exists in some other programs. We will lint it later (or earlier) in the loop. |
| 170 | + continue; |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + const contents = getFileContents(file); |
| 175 | + |
| 176 | + // Only check for a new tslint config if the path changes. |
| 177 | + const currentDirectory = path.dirname(file); |
| 178 | + if (currentDirectory !== lastDirectory) { |
| 179 | + configLoad = Configuration.findConfiguration(tslintConfigPath, file); |
| 180 | + lastDirectory = currentDirectory; |
| 181 | + } |
| 182 | + |
| 183 | + if (configLoad) { |
| 184 | + // Give some breathing space to other promises that might be waiting. |
| 185 | + await Promise.resolve(); |
| 186 | + linter.lint(file, contents, configLoad.results); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + return linter.getResult(); |
| 191 | +} |
| 192 | + |
| 193 | +function getFilesToLint( |
| 194 | + root: string, |
| 195 | + options: TslintBuilderOptions, |
| 196 | + linter: typeof tslint.Linter, |
| 197 | + program?: ts.Program, |
| 198 | +): string[] { |
| 199 | + const ignore = options.exclude; |
| 200 | + const files = options.files || []; |
| 201 | + |
| 202 | + if (files.length > 0) { |
| 203 | + return files |
| 204 | + .map(file => glob.sync(file, { cwd: root, ignore, nodir: true })) |
| 205 | + .reduce((prev, curr) => prev.concat(curr), []) |
| 206 | + .map(file => path.join(root, file)); |
| 207 | + } |
| 208 | + |
| 209 | + if (!program) { |
| 210 | + return []; |
| 211 | + } |
| 212 | + |
| 213 | + let programFiles = linter.getFileNames(program); |
| 214 | + |
| 215 | + if (ignore && ignore.length > 0) { |
| 216 | + // normalize to support ./ paths |
| 217 | + const ignoreMatchers = ignore |
| 218 | + .map(pattern => new Minimatch(path.normalize(pattern), { dot: true })); |
| 219 | + |
| 220 | + programFiles = programFiles |
| 221 | + .filter(file => !ignoreMatchers.some(matcher => matcher.match(path.relative(root, file)))); |
| 222 | + } |
| 223 | + |
| 224 | + return programFiles; |
| 225 | +} |
| 226 | + |
| 227 | +function getFileContents(file: string): string { |
| 228 | + // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. |
| 229 | + try { |
| 230 | + return stripBom(readFileSync(file, 'utf-8')); |
| 231 | + } catch { |
| 232 | + throw new Error(`Could not read file '${file}'.`); |
| 233 | + } |
| 234 | +} |
0 commit comments