diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts index 5c6118edeb2ec..b5849c0f8485f 100644 --- a/src/dev/eslint/lint_files.ts +++ b/src/dev/eslint/lint_files.ts @@ -12,6 +12,45 @@ import { REPO_ROOT } from '@kbn/utils'; import { createFailError, ToolingLog } from '@kbn/dev-utils'; import { File } from '../file'; +// For files living on the filesystem +function lintFilesOnFS(cli: CLIEngine, files: File[]) { + const paths = files.map((file) => file.getRelativePath()); + return cli.executeOnFiles(paths); +} + +// For files living somewhere else (ie. git object) +async function lintFilesOnContent(cli: CLIEngine, files: File[]) { + const report: { + results: any[]; + errorCount: number; + warningCount: number; + fixableErrorCount: number; + fixableWarningCount: number; + } = { + results: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + }; + + for (let i = 0; i < files.length; i++) { + const r = cli.executeOnText(await files[i].getContent(), files[i].getRelativePath()); + // Despite a relative path was given, the result would contain an absolute one. Work around it. + r.results[0].filePath = r.results[0].filePath.replace( + files[i].getAbsolutePath(), + files[i].getRelativePath() + ); + report.results.push(...r.results); + report.errorCount += r.errorCount; + report.warningCount += r.warningCount; + report.fixableErrorCount += r.fixableErrorCount; + report.fixableWarningCount += r.fixableWarningCount; + } + + return report; +} + /** * Lints a list of files with eslint. eslint reports are written to the log * and a FailError is thrown when linting errors occur. @@ -20,15 +59,16 @@ import { File } from '../file'; * @param {Array} files * @return {undefined} */ -export function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { +export async function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { const cli = new CLIEngine({ cache: true, cwd: REPO_ROOT, fix, }); - const paths = files.map((file) => file.getRelativePath()); - const report = cli.executeOnFiles(paths); + const virtualFilesCount = files.filter((file) => file.isVirtual()).length; + const report = + virtualFilesCount && !fix ? await lintFilesOnContent(cli, files) : lintFilesOnFS(cli, files); if (fix) { CLIEngine.outputFixes(report); diff --git a/src/dev/file.ts b/src/dev/file.ts index b532a7bb70602..01005b257a403 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -7,11 +7,13 @@ */ import { dirname, extname, join, relative, resolve, sep, basename } from 'path'; +import { createFailError } from '@kbn/dev-utils'; export class File { private path: string; private relativePath: string; private ext: string; + private fileReader: undefined | (() => Promise); constructor(path: string) { this.path = resolve(path); @@ -55,6 +57,11 @@ export class File { ); } + // Virtual files cannot be read as usual, an helper is needed + public isVirtual() { + return this.fileReader !== undefined; + } + public getRelativeParentDirs() { const parents: string[] = []; @@ -81,4 +88,15 @@ export class File { public toJSON() { return this.relativePath; } + + public setFileReader(fileReader: () => Promise) { + this.fileReader = fileReader; + } + + public getContent() { + if (this.fileReader) { + return this.fileReader(); + } + throw createFailError('getContent() was invoked on a non-virtual File'); + } } diff --git a/src/dev/precommit_hook/get_files_for_commit.js b/src/dev/precommit_hook/get_files_for_commit.js index 44c8c9d5e6bc0..52dfab49c5c64 100644 --- a/src/dev/precommit_hook/get_files_for_commit.js +++ b/src/dev/precommit_hook/get_files_for_commit.js @@ -6,12 +6,65 @@ * Side Public License, v 1. */ +import { format } from 'util'; import SimpleGit from 'simple-git'; import { fromNode as fcb } from 'bluebird'; import { REPO_ROOT } from '@kbn/utils'; import { File } from '../file'; +/** + * Return the `git diff` argument used for building the list of files + * + * @param {String} gitRef + * @return {String} + * + * gitRef return + * '' '--cached' + * '' '~1..' + * '..' '..' + * '...' '...' + * '..' '..' + * '...' '...' + * '..' '..' + * '...' '...' + */ +function getRefForDiff(gitRef) { + if (!gitRef) { + return '--cached'; + } else if (gitRef.includes('..')) { + return gitRef; + } else { + return format('%s~1..%s', gitRef, gitRef); + } +} + +/** + * Return the used for reading files content + * + * @param {String} gitRef + * @return {String} + * + * gitRef return + * '' '' + * '' '' + * '..' 'HEAD' + * '...' 'HEAD' + * '..' '' + * '...' '' + * '..' '' + * '...' '' + */ +function getRefForCat(gitRef) { + if (!gitRef) { + return ''; + } else if (gitRef.includes('..')) { + return gitRef.endsWith('..') ? 'HEAD' : gitRef.slice(gitRef.lastIndexOf('..') + 2); + } else { + return gitRef; + } +} + /** * Get the files that are staged for commit (excluding deleted files) * as `File` objects that are aware of their commit status. @@ -21,29 +74,23 @@ import { File } from '../file'; */ export async function getFilesForCommit(gitRef) { const simpleGit = new SimpleGit(REPO_ROOT); - const gitRefForDiff = gitRef ? gitRef : '--cached'; - const output = await fcb((cb) => simpleGit.diff(['--name-status', gitRefForDiff], cb)); + const gitRefForDiff = getRefForDiff(gitRef); + const gitRefForCat = getRefForCat(gitRef); + + const output = await fcb((cb) => { + simpleGit.diff(['--diff-filter=d', '--name-only', gitRefForDiff], cb); + }); return ( output .split('\n') // Ignore blank lines .filter((line) => line.trim().length > 0) - // git diff --name-status outputs lines with two OR three parts - // separated by a tab character - .map((line) => line.trim().split('\t')) - .map(([status, ...paths]) => { - // ignore deleted files - if (status === 'D') { - return undefined; - } - - // the status is always in the first column - // .. If the file is edited the line will only have two columns - // .. If the file is renamed it will have three columns - // .. In any case, the last column is the CURRENT path to the file - return new File(paths[paths.length - 1]); + .map((path) => { + const file = new File(path); + const object = format('%s:%s', gitRefForCat, path); + file.setFileReader(() => fcb((cb) => simpleGit.catFile(['-p', object], cb))); + return file; }) - .filter(Boolean) ); } diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index 73394a62e3396..e1eafaf28d95d 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { run, combineErrors, createFlagError } from '@kbn/dev-utils'; +import { run, combineErrors, createFlagError, createFailError } from '@kbn/dev-utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; import { getFilesForCommit, checkFileCasing } from './precommit_hook'; @@ -23,6 +23,11 @@ run( throw createFlagError('expected --max-files to be a number greater than 0'); } + const virtualFilesCount = files.filter((file) => file.isVirtual()).length; + if (virtualFilesCount > 0 && virtualFilesCount < files.length) { + throw createFailError('Mixing of virtual and on-filesystem files is unsupported'); + } + if (maxFilesCount && files.length > maxFilesCount) { log.warning( `--max-files is set to ${maxFilesCount} and ${files.length} were discovered. The current script execution will be skipped.` @@ -66,7 +71,11 @@ run( help: ` --fix Execute eslint in --fix mode --max-files Max files number to check against. If exceeded the script will skip the execution - --ref Run checks against any git ref files (example HEAD or ) instead of running against staged ones + --ref Run checks against git ref files instead of running against staged ones + Examples: + HEAD~1..HEAD files changed in the commit at HEAD + HEAD equivalent to HEAD~1..HEAD + main... files changed in current branch since the common ancestor with main `, }, } diff --git a/src/dev/stylelint/lint_files.js b/src/dev/stylelint/lint_files.js index 6e62c85d44ae8..1ebc981728814 100644 --- a/src/dev/stylelint/lint_files.js +++ b/src/dev/stylelint/lint_files.js @@ -16,6 +16,51 @@ import { createFailError } from '@kbn/dev-utils'; const stylelintPath = path.resolve(__dirname, '..', '..', '..', '.stylelintrc'); const styleLintConfig = safeLoad(fs.readFileSync(stylelintPath)); +// For files living on the filesystem +function lintFilesOnFS(files) { + const paths = files.map((file) => file.getRelativePath()); + + const options = { + files: paths, + config: styleLintConfig, + formatter: 'string', + ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), + }; + + return stylelint.lint(options); +} + +// For files living somewhere else (ie. git object) +async function lintFilesOnContent(files) { + const report = { + errored: false, + output: '', + postcssResults: [], + results: [], + maxWarningsExceeded: { + maxWarnings: 0, + foundWarnings: 0, + }, + }; + + for (let i = 0; i < files.length; i++) { + const options = { + code: await files[i].getContent(), + config: styleLintConfig, + formatter: 'string', + ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), + }; + const r = await stylelint.lint(options); + report.errored = report.errored || r.errored; + report.output += r.output.replace(//, files[i].getRelativePath()).slice(0, -1); + report.postcssResults.push(...(r.postcssResults || [])); + report.maxWarnings = r.maxWarnings; + report.foundWarnings += r.foundWarnings; + } + + return report; +} + /** * Lints a list of files with eslint. eslint reports are written to the log * and a FailError is thrown when linting errors occur. @@ -25,16 +70,9 @@ const styleLintConfig = safeLoad(fs.readFileSync(stylelintPath)); * @return {undefined} */ export async function lintFiles(log, files) { - const paths = files.map((file) => file.getRelativePath()); - - const options = { - files: paths, - config: styleLintConfig, - formatter: 'string', - ignorePath: path.resolve(__dirname, '..', '..', '..', '.stylelintignore'), - }; + const virtualFilesCount = files.filter((file) => file.isVirtual()).length; + const report = virtualFilesCount ? await lintFilesOnContent(files) : await lintFilesOnFS(files); - const report = await stylelint.lint(options); if (report.errored) { log.error(report.output); throw createFailError('[stylelint] errors');