Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions src/dev/eslint/lint_files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,15 +59,16 @@ import { File } from '../file';
* @param {Array<File>} 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);
Expand Down
18 changes: 18 additions & 0 deletions src/dev/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>);

constructor(path: string) {
this.path = resolve(path);
Expand Down Expand Up @@ -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[] = [];

Expand All @@ -81,4 +88,15 @@ export class File {
public toJSON() {
return this.relativePath;
}

public setFileReader(fileReader: () => Promise<string>) {
this.fileReader = fileReader;
}

public getContent() {
if (this.fileReader) {
return this.fileReader();
}
throw createFailError('getContent() was invoked on a non-virtual File');
}
}
81 changes: 64 additions & 17 deletions src/dev/precommit_hook/get_files_for_commit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
* '<ref>' '<ref>~1..<ref>'
* '<ref>..' '<ref>..'
* '<ref>...' '<ref>...'
* '..<ref>' '..<ref>'
* '...<ref>' '...<ref>'
* '<ref_A>..<ref_B>' '<ref_A>..<ref_B>'
* '<ref_A>...<ref_B>' '<ref_A>...<ref_B>'
*/
function getRefForDiff(gitRef) {
if (!gitRef) {
return '--cached';
} else if (gitRef.includes('..')) {
return gitRef;
} else {
return format('%s~1..%s', gitRef, gitRef);
}
}

/**
* Return the <ref> used for reading files content
*
* @param {String} gitRef
* @return {String}
*
* gitRef return
* '' ''
* '<ref>' '<ref>'
* '<ref>..' 'HEAD'
* '<ref>...' 'HEAD'
* '..<ref>' '<ref>'
* '...<ref>' '<ref>'
* '<ref_A>..<ref_B>' '<ref_B>'
* '<ref_A>...<ref_B>' '<ref_B>'
*/
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.
Expand All @@ -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)
);
}
13 changes: 11 additions & 2 deletions src/dev/run_precommit_hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.`
Expand Down Expand Up @@ -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 <commit_sha>) 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
`,
},
}
Expand Down
56 changes: 47 additions & 9 deletions src/dev/stylelint/lint_files.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<input css \d+>/, 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.
Expand All @@ -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');
Expand Down