Skip to content

Commit

Permalink
feat: support config.eslintrc as function
Browse files Browse the repository at this point in the history
- eslintrc can be lazy initialized
- function is called with current repository and its location on filesystem
  • Loading branch information
AriPerkkio committed Jul 6, 2021
1 parent bd294f5 commit 6079cab
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 57 deletions.
34 changes: 17 additions & 17 deletions README.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export interface Config {
| ((ruleId: string, options: { repository: string }) => boolean);
resultParser: ResultParser;
concurrentTasks: number;
eslintrc: Linter.Config;
eslintrc:
| Linter.Config
| ((options?: {
repository: string;
location: string;
}) => Linter.Config);
CI: boolean;
logLevel: LogLevel;
cache: boolean;
Expand Down
11 changes: 9 additions & 2 deletions lib/config/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,26 @@ export default async function validate(

errors.push(validateStringArray('extensions', extensions));

if (!eslintrc || Object.keys(eslintrc).length === 0) {
if (!eslintrc) {
errors.push(`Missing eslintrc.`);
} else {
try {
// This will throw when eslintrc is invalid
const linter = new ESLint({
useEslintrc: false,
overrideConfig: eslintrc,
overrideConfig:
typeof eslintrc === 'function' ? eslintrc() : eslintrc,
});

errors.push(await validateEslintRules(linter));
} catch (e) {
errors.push(`eslintrc: ${e.message}`);

if (typeof eslintrc === 'function') {
errors.push(
'Note that "config.eslintrc" is called with empty options during configuration validation.'
);
}
}
}

Expand Down
42 changes: 28 additions & 14 deletions lib/engine/worker-task.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import fs from 'fs';
import { parentPort, workerData } from 'worker_threads';
import { resolve } from 'path';
import { ESLint, Linter } from 'eslint';
import { codeFrameColumns, SourceLocation } from '@babel/code-frame';

import { LintMessage, WorkerData } from './types';
import config from '@config';
import { getFiles, removeCachedRepository, SourceFile } from '@file-client';
import {
CACHE_LOCATION,
getFiles,
removeCachedRepository,
SourceFile,
} from '@file-client';

export type WorkerMessage =
| { type: 'START' }
Expand Down Expand Up @@ -183,19 +189,6 @@ const postMessage = (message: WorkerMessage) => {
* - Keep progress-logger up-to-date of status via onMessage
*/
export default async function workerTask(): Promise<void> {
const linter = new ESLint({
useEslintrc: false,
overrideConfig: config.eslintrc,

// Only rules set in configuration are expected.
// Ignore all inline configurations found from target repositories.
allowInlineConfig: false,

// Lint all given files, ignore none. Cache is located under node_modules.
// config.pathIgnorePattern is used for exclusions.
ignore: false,
});

const { repository } = workerData as WorkerData;
const messageReducer = getMessageReducer(repository);

Expand All @@ -209,6 +202,27 @@ export default async function workerTask(): Promise<void> {
onReadFailure: () => postMessage({ type: 'READ_FAILURE' }),
});

const eslintrc =
typeof config.eslintrc === 'function'
? config.eslintrc({
repository,
location: resolve(`${CACHE_LOCATION}/${repository}`),
})
: config.eslintrc;

const linter = new ESLint({
useEslintrc: false,
overrideConfig: eslintrc,

// Only rules set in configuration are expected.
// Ignore all inline configurations found from target repositories.
allowInlineConfig: false,

// Lint all given files, ignore none. Cache is located under node_modules.
// config.pathIgnorePattern is used for exclusions.
ignore: false,
});

postMessage({ type: 'LINT_START', payload: files.length });

for (const [index, file] of files.entries()) {
Expand Down
84 changes: 62 additions & 22 deletions test/integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
REPOSITORY_CACHE,
} from '../utils';

const DEBUG_LOG_PATTERN = /\[DEBUG (\S|:)*\] /g;

describe('integration', () => {
test('results are rendered on CI mode', async () => {
const { output } = await runProductionBuild({ CI: true });
Expand Down Expand Up @@ -1065,36 +1067,74 @@ describe('integration', () => {
});

const finalLog = output.pop();
const withoutTimestamps = finalLog!.replace(/\[DEBUG (\S|:)*\]/g, '');
const withoutTimestamps = finalLog!.replace(DEBUG_LOG_PATTERN, '');

expect(withoutTimestamps).toMatchInlineSnapshot(`
"Full log:
no-undef - AriPerkkio/eslint-remote-tester-integration-test-target
no-var - AriPerkkio/eslint-remote-tester-integration-test-target
no-implicit-globals - AriPerkkio/eslint-remote-tester-integration-test-target
no-undef - AriPerkkio/eslint-remote-tester-integration-test-target
no-empty - AriPerkkio/eslint-remote-tester-integration-test-target
one-var - AriPerkkio/eslint-remote-tester-integration-test-target
vars-on-top - AriPerkkio/eslint-remote-tester-integration-test-target
no-var - AriPerkkio/eslint-remote-tester-integration-test-target
no-implicit-globals - AriPerkkio/eslint-remote-tester-integration-test-target
id-length - AriPerkkio/eslint-remote-tester-integration-test-target
quote-props - AriPerkkio/eslint-remote-tester-integration-test-target
getter-return - AriPerkkio/eslint-remote-tester-integration-test-target
space-before-function-paren - AriPerkkio/eslint-remote-tester-integration-test-target
strict - AriPerkkio/eslint-remote-tester-integration-test-target
space-before-blocks - AriPerkkio/eslint-remote-tester-integration-test-target
capitalized-comments - AriPerkkio/eslint-remote-tester-integration-test-target
no-compare-neg-zero - AriPerkkio/eslint-remote-tester-integration-test-target
no-magic-numbers - AriPerkkio/eslint-remote-tester-integration-test-target
indent - AriPerkkio/eslint-remote-tester-integration-test-target
capitalized-comments - AriPerkkio/eslint-remote-tester-integration-test-target
eol-last - AriPerkkio/eslint-remote-tester-integration-test-target
no-undef - AriPerkkio/eslint-remote-tester-integration-test-target
no-var - AriPerkkio/eslint-remote-tester-integration-test-target
no-implicit-globals - AriPerkkio/eslint-remote-tester-integration-test-target
no-undef - AriPerkkio/eslint-remote-tester-integration-test-target
no-empty - AriPerkkio/eslint-remote-tester-integration-test-target
one-var - AriPerkkio/eslint-remote-tester-integration-test-target
vars-on-top - AriPerkkio/eslint-remote-tester-integration-test-target
no-var - AriPerkkio/eslint-remote-tester-integration-test-target
no-implicit-globals - AriPerkkio/eslint-remote-tester-integration-test-target
id-length - AriPerkkio/eslint-remote-tester-integration-test-target
quote-props - AriPerkkio/eslint-remote-tester-integration-test-target
getter-return - AriPerkkio/eslint-remote-tester-integration-test-target
space-before-function-paren - AriPerkkio/eslint-remote-tester-integration-test-target
strict - AriPerkkio/eslint-remote-tester-integration-test-target
space-before-blocks - AriPerkkio/eslint-remote-tester-integration-test-target
capitalized-comments - AriPerkkio/eslint-remote-tester-integration-test-target
no-compare-neg-zero - AriPerkkio/eslint-remote-tester-integration-test-target
no-magic-numbers - AriPerkkio/eslint-remote-tester-integration-test-target
indent - AriPerkkio/eslint-remote-tester-integration-test-target
capitalized-comments - AriPerkkio/eslint-remote-tester-integration-test-target
eol-last - AriPerkkio/eslint-remote-tester-integration-test-target
[ERROR] AriPerkkio/eslint-remote-tester-integration-test-target 21 errors
[DONE] Finished scan of 1 repositories
[INFO] Cached repositories (1) at ./node_modules/.cache-eslint-remote-tester
"
`);
});

test('calls eslintrc function with repository and its location', async () => {
const { output } = await runProductionBuild({
CI: false,
logLevel: 'verbose',
eslintrc: options => {
const { parentPort } = require('worker_threads');

if (parentPort) {
parentPort.postMessage({
type: 'DEBUG',
payload: `location: ${options?.location}`,
});

parentPort.postMessage({
type: 'DEBUG',
payload: `repository: ${options?.repository}`,
});
}

return { root: true, extends: ['eslint:all'] };
},
});

const finalLog = output.pop();
const withoutTimestamps = finalLog!.replace(DEBUG_LOG_PATTERN, '');

expect(withoutTimestamps).toMatchInlineSnapshot(`
"Full log:
location: <removed>/node_modules/.cache-eslint-remote-tester/AriPerkkio/eslint-remote-tester-integration-test-target
repository: AriPerkkio/eslint-remote-tester-integration-test-target
[ERROR] AriPerkkio/eslint-remote-tester-integration-test-target 5 errors
[DONE] Finished scan of 1 repositories
[INFO] Cached repositories (1) at ./node_modules/.cache-eslint-remote-tester
"
`);
});
});
28 changes: 27 additions & 1 deletion test/unit/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,20 @@ describe('validateConfig', () => {
});
});

test('eslintrc accepts function', async () => {
await validateConfig({
...DEFAULT_CONFIGURATION,
eslintrc: () => ({ rules: {} }),
});
});

test('rulesUnderTesting is optional', async () => {
await validateConfig({
...DEFAULT_CONFIGURATION,
rulesUnderTesting: undefined,
});
});

test('rulesUnderTesting accepts empty array', async () => {
await validateConfig({
...DEFAULT_CONFIGURATION,
Expand Down Expand Up @@ -480,7 +488,7 @@ describe('validateConfig', () => {
expect(validationError).toMatch('Missing eslintrc');
});

test('eslintrc is validated', async () => {
test('eslintrc as object is validated', async () => {
await validateConfig({
...DEFAULT_CONFIGURATION,
eslintrc: { 'not-valid-key': true } as any,
Expand All @@ -495,6 +503,24 @@ describe('validateConfig', () => {
);
});

test('eslintrc as function is validated', async () => {
await validateConfig({
...DEFAULT_CONFIGURATION,
eslintrc: () => ({ 'not-valid-key': true } as any),
});

const [validationError] = getConsoleLogCalls();
expect(validationError).toMatch(
'eslintrc: ESLint configuration in CLIOptions is invalid'
);
expect(validationError).toMatch(
'Unexpected top-level property "not-valid-key"'
);
expect(validationError).toMatch(
'Note that "config.eslintrc" is called with empty options during configuration validation.'
);
});

test('rulesUnderTesting requires an array or function', async () => {
const rulesUnderTesting: any = { length: 10 };
await validateConfig({
Expand Down
5 changes: 5 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const LAST_RENDER_PATTERN = /(Results|Full log)[\s|\S]*/;
const COMPARISON_RESULTS_PATTERN = /(Comparison results:[\s|\S]*)Results/;
const ON_COMPLETE_PATTERN = /("onComplete": )"([\s|\S]*)"/;
const RULES_UNDER_TESTING_PATTERN = /("rulesUnderTesting": )"([\s|\S]*)",/;
const ESLINTRC_PATTERN = /("eslintrc": )"([\s|\S]*)",/;
const ESCAPED_NEWLINE_PATTERN = /\\n/g;
const DEBUG_LOG = '/tmp/test.debug.log';

Expand All @@ -50,10 +51,14 @@ function createConfiguration(
if (typeof options.rulesUnderTesting === 'function') {
config.rulesUnderTesting = options.rulesUnderTesting.toString() as any;
}
if (typeof options.eslintrc === 'function') {
config.eslintrc = options.eslintrc.toString() as any;
}

const configText = JSON.stringify(config, null, 4)
.replace(ON_COMPLETE_PATTERN, '$1$2')
.replace(RULES_UNDER_TESTING_PATTERN, '$1$2,')
.replace(ESLINTRC_PATTERN, '$1$2,')
.replace(ESCAPED_NEWLINE_PATTERN, '\n');

fs.writeFileSync(name, `module.exports=${configText}`, 'utf8');
Expand Down

0 comments on commit 6079cab

Please sign in to comment.