Skip to content

Commit

Permalink
feat(experimental-utils): expose our RuleTester extension
Browse files Browse the repository at this point in the history
There are a few things we've built in (like automatically adding a filename) that others might find value in.
Plus I want to use it in the internal plugin.
  • Loading branch information
bradzacher committed Apr 29, 2020
1 parent 383f931 commit 3e7e2a2
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 116 deletions.
26 changes: 7 additions & 19 deletions packages/eslint-plugin-internal/tests/RuleTester.ts
@@ -1,22 +1,10 @@
import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils';
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
import path from 'path';

const { batchedSingleLineTests } = ESLintUtils;

const parser = '@typescript-eslint/parser';

type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};
class RuleTester extends TSESLint.RuleTester {
// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
constructor(options: RuleTesterConfig) {
super({
...options,
parser: require.resolve(options.parser),
});
}
function getFixturesRootDir(): string {
return path.join(__dirname, 'fixtures');
}

export { RuleTester, batchedSingleLineTests };
const { batchedSingleLineTests, RuleTester } = ESLintUtils;

export { RuleTester, batchedSingleLineTests, getFixturesRootDir };
100 changes: 3 additions & 97 deletions packages/eslint-plugin/tests/RuleTester.ts
@@ -1,104 +1,10 @@
import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils';
import { clearCaches } from '@typescript-eslint/parser';
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
import * as path from 'path';

const parser = '@typescript-eslint/parser';

type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};
class RuleTester extends TSESLint.RuleTester {
// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
constructor(private readonly options: RuleTesterConfig) {
super({
...options,
parser: require.resolve(options.parser),
});
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
}`;
if (options.project) {
return path.join(getFixturesRootDir(), filename);
}

return filename;
} else if (this.options.parserOptions) {
return this.getFilename(this.options.parserOptions);
}

return 'file.ts';
}

// as of eslint 6 you have to provide an absolute path to the parser
// If you don't do that at the test level, the test will fail somewhat cryptically...
// This is a lot more explicit
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
name: string,
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
tests: TSESLint.RunTests<TMessageIds, TOptions>,
): void {
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;

// standardize the valid tests as objects
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
};
}
return test;
});

tests.valid.forEach(test => {
if (typeof test !== 'string') {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.getFilename(test.parserOptions);
}
}
});
tests.invalid.forEach(test => {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.getFilename(test.parserOptions);
}
});

super.run(name, rule, tests);
}
}

function getFixturesRootDir(): string {
return path.join(process.cwd(), 'tests/fixtures/');
return path.join(__dirname, 'fixtures');
}

const { batchedSingleLineTests } = ESLintUtils;

// make sure that the parser doesn't hold onto file handles between tests
// on linux (i.e. our CI env), there can be very a limited number of watch handles available
afterAll(() => {
clearCaches();
});

/**
* Simple no-op tag to mark code samples as "should not format with prettier"
* for the internal/plugin-test-formatting lint rule
*/
function noFormat(strings: TemplateStringsArray, ...keys: string[]): string {
const lastIndex = strings.length - 1;
return (
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
strings[lastIndex]
);
}
const { batchedSingleLineTests, RuleTester, noFormat } = ESLintUtils;

export { batchedSingleLineTests, getFixturesRootDir, noFormat, RuleTester };
109 changes: 109 additions & 0 deletions packages/experimental-utils/src/eslint-utils/RuleTester.ts
@@ -0,0 +1,109 @@
import * as TSESLint from '../ts-eslint';
import * as path from 'path';

const parser = '@typescript-eslint/parser';

type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};

class RuleTester extends TSESLint.RuleTester {
// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
constructor(private readonly options: RuleTesterConfig) {
super({
...options,
parser: require.resolve(options.parser),
});

// make sure that the parser doesn't hold onto file handles between tests
// on linux (i.e. our CI env), there can be very a limited number of watch handles available
afterAll(() => {
try {
// instead of creating a hard dependency, just use a soft require
// a bit weird, but if they're using this tooling, it'll be installed
require(parser).clearCaches();
} catch {
// ignored
}
});
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
}`;
if (options.project) {
return path.join(
options.tsconfigRootDir != null
? options.tsconfigRootDir
: process.cwd(),
filename,
);
}

return filename;
} else if (this.options.parserOptions) {
return this.getFilename(this.options.parserOptions);
}

return 'file.ts';
}

// as of eslint 6 you have to provide an absolute path to the parser
// If you don't do that at the test level, the test will fail somewhat cryptically...
// This is a lot more explicit
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
name: string,
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
tests: TSESLint.RunTests<TMessageIds, TOptions>,
): void {
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;

// standardize the valid tests as objects
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
};
}
return test;
});

tests.valid.forEach(test => {
if (typeof test !== 'string') {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.getFilename(test.parserOptions);
}
}
});
tests.invalid.forEach(test => {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.getFilename(test.parserOptions);
}
});

super.run(name, rule, tests);
}
}

/**
* Simple no-op tag to mark code samples as "should not format with prettier"
* for the internal/plugin-test-formatting lint rule
*/
function noFormat(strings: TemplateStringsArray, ...keys: string[]): string {
const lastIndex = strings.length - 1;
return (
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
strings[lastIndex]
);
}

export { noFormat, RuleTester };
1 change: 1 addition & 0 deletions packages/experimental-utils/src/eslint-utils/index.ts
Expand Up @@ -2,4 +2,5 @@ export * from './applyDefault';
export * from './batchedSingleLineTests';
export * from './getParserServices';
export * from './RuleCreator';
export * from './RuleTester';
export * from './deepMerge';

0 comments on commit 3e7e2a2

Please sign in to comment.