Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: build recommended ruleset based on rules meta doc property #615

Merged
merged 1 commit into from Jul 5, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 30 additions & 37 deletions src/index.ts
@@ -1,8 +1,13 @@
import { readdirSync } from 'fs';
import { join, parse } from 'path';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import globals from './globals.json';
import * as snapshotProcessor from './processors/snapshot-processor';

type RuleModule = TSESLint.RuleModule<string, unknown[]> & {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use this type for all rules where we declare them as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean?

Ideally we should try and use the inferred type that's returned from createRule as much as possible for each rule, as that has the proper types for their message ids & options.

We could switch to using a barrel export like @typescript-eslint does, where we have index.ts that explicitly exports each rule:

import noIfs from './no-ifs';

export default  {
  'no-ifs': noIfs,
}

That would let us import all the rules here as a single default import.

we can even script it
#!/usr/bin/env ts-node-transpile-only

import * as fs from 'fs';
import * as path from 'path';
import prettier from 'prettier';
import { prettier as prettierConfig } from '../package.json';

const toCamelCase = (str: string) => {
  const words = str.split('-');

  return [
    words.shift(),
    ...words.map(w => w.substring(0, 1).toUpperCase() + w.substring(1)),
  ].join('');
};

const excludedFiles = ['__tests__', 'utils', 'index'];

const ruleFiles = fs
  .readdirSync(path.resolve(__dirname, '../src/rules'))
  .map(file => path.parse(file).name)
  .filter(file => !excludedFiles.includes(file));

const contents = [
  ...ruleFiles.map(rule => `import ${toCamelCase(rule)} from './${rule}';`),
  '',
  'export default {',
  ...ruleFiles.map(rule => `  '${rule}': ${toCamelCase(rule)},`),
  '};', //
].join('\n');

fs.writeFileSync(
  path.resolve(__dirname, '../src/rules/index.ts'),
  prettier.format(contents, { parser: 'typescript', ...prettierConfig }),
);

Copy link
Collaborator Author

@G-Rath G-Rath Jul 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm going to merge this, but happy to continue this discussion 🙂)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant doing something here so we use the same type:

export const createRule = ESLintUtils.RuleCreator(name => {
const ruleName = parsePath(name).name;
return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`;
});

Not sure how feasible it is, tho?

meta: Required<Pick<TSESLint.RuleMetaData<string>, 'docs'>>;
};

// can be removed once we've on v3: https://github.com/typescript-eslint/typescript-eslint/issues/2060
declare module '@typescript-eslint/experimental-utils/dist/ts-eslint/Rule' {
export interface RuleMetaDataDocs {
Expand All @@ -25,50 +30,38 @@ const excludedFiles = ['__tests__', 'utils'];
const rules = readdirSync(rulesDir)
.map(rule => parse(rule).name)
.filter(rule => !excludedFiles.includes(rule))
.reduce<Record<string, RuleModule>>(
(acc, curr) => ({
...acc,
[curr]: importDefault(join(rulesDir, curr)) as RuleModule,
}),
{},
);

const recommendedRules = Object.entries(rules)
.filter(([, rule]) => rule.meta.docs.recommended)
.reduce(
(acc, curr) =>
Object.assign(acc, { [curr]: importDefault(join(rulesDir, curr)) }),
(acc, [name, rule]) => ({
...acc,
[`jest/${name}`]: rule.meta.docs.recommended,
}),
{},
);

const allRules = Object.keys(rules).reduce<Record<string, string>>(
(rules, key) => ({ ...rules, [`jest/${key}`]: 'error' }),
{},
);
const allRules = Object.keys(rules).reduce<
Record<string, TSESLint.Linter.RuleLevel>
>((rules, key) => ({ ...rules, [`jest/${key}`]: 'error' }), {});

const createConfig = (rules: Record<string, TSESLint.Linter.RuleLevel>) => ({
plugins: ['jest'],
env: { 'jest/globals': true },
rules,
});

export = {
configs: {
all: {
plugins: ['jest'],
env: {
'jest/globals': true,
},
rules: allRules,
},
recommended: {
plugins: ['jest'],
env: {
'jest/globals': true,
},
rules: {
'jest/expect-expect': 'warn',
'jest/no-commented-out-tests': 'warn',
'jest/no-disabled-tests': 'warn',
'jest/no-export': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/no-jest-import': 'error',
'jest/no-mocks-import': 'error',
'jest/no-jasmine-globals': 'warn',
'jest/no-standalone-expect': 'error',
'jest/no-test-callback': 'error',
'jest/no-test-prefixes': 'error',
'jest/no-try-expect': 'error',
'jest/valid-describe': 'error',
'jest/valid-expect': 'error',
'jest/valid-expect-in-promise': 'error',
},
},
all: createConfig(allRules),
recommended: createConfig(recommendedRules),
style: {
plugins: ['jest'],
rules: {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/expect-expect.ts
Expand Up @@ -49,7 +49,7 @@ export default createRule<
docs: {
category: 'Best Practices',
description: 'Enforce assertion to be made in a test body',
recommended: false,
recommended: 'warn',
},
messages: {
noAssertions: 'Test has no assertions',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-alias-methods.ts
Expand Up @@ -6,7 +6,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Disallow alias methods',
recommended: 'warn',
recommended: false,
},
messages: {
replaceAlias: `Replace {{ alias }}() with its canonical name of {{ canonical }}()`,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-commented-out-tests.ts
Expand Up @@ -13,7 +13,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Disallow commented out tests',
recommended: false,
recommended: 'warn',
},
messages: {
commentedTests: 'Some tests seem to be commented',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-disabled-tests.ts
Expand Up @@ -6,7 +6,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Disallow disabled tests',
recommended: false,
recommended: 'warn',
},
messages: {
missingFunction: 'Test is missing function argument',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-export.ts
Expand Up @@ -10,7 +10,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Prevent exporting from test files',
recommended: false,
recommended: 'error',
},
messages: {
unexpectedExport: `Do not export from a test file.`,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-focused-tests.ts
Expand Up @@ -48,7 +48,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Disallow focused tests',
recommended: false,
recommended: 'error',
},
messages: {
focusedTest: 'Unexpected focused test.',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-jasmine-globals.ts
Expand Up @@ -12,7 +12,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Disallow Jasmine globals',
recommended: 'error',
recommended: 'warn',
},
messages: {
illegalGlobal:
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-standalone-expect.ts
Expand Up @@ -66,7 +66,7 @@ export default createRule<
docs: {
category: 'Best Practices',
description: 'Prevents expects that are outside of an it or test block.',
recommended: false,
recommended: 'error',
},
messages: {
unexpectedExpect: 'Expect must be inside of a test block.',
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-test-callback.ts
Expand Up @@ -7,7 +7,7 @@ export default createRule({
docs: {
category: 'Best Practices',
description: 'Avoid using a callback in asynchronous tests',
recommended: false,
recommended: 'error',
suggestion: true,
},
messages: {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-try-expect.ts
Expand Up @@ -12,7 +12,7 @@ export default createRule({
docs: {
description: 'Prefer using toThrow for exception tests',
category: 'Best Practices',
recommended: false,
recommended: 'error',
},
deprecated: true,
replacedBy: ['no-conditional-expect'],
Expand Down
2 changes: 1 addition & 1 deletion src/rules/valid-describe.ts
Expand Up @@ -29,7 +29,7 @@ export default createRule({
docs: {
category: 'Possible Errors',
description: 'Enforce valid `describe()` callback',
recommended: 'warn',
recommended: 'error',
},
messages: {
nameAndCallback: 'Describe requires name and callback arguments',
Expand Down
8 changes: 5 additions & 3 deletions tools/generate-rules-table.ts
Expand Up @@ -14,7 +14,9 @@ interface RuleDetails {
fixable: FixType | false;
}

type RuleModule = TSESLint.RuleModule<string, unknown[]>;
type RuleModule = TSESLint.RuleModule<string, unknown[]> & {
meta: Required<Pick<TSESLint.RuleMetaData<string>, 'docs'>>;
};

const staticElements = {
listHeaderRow: ['Rule', 'Description', 'Configurations', 'Fixable'],
Expand Down Expand Up @@ -97,10 +99,10 @@ const details: RuleDetails[] = Object.keys(config.configs.all.rules)
.map(
([name, rule]): RuleDetails => ({
name,
description: rule.meta.docs?.description ?? '',
description: rule.meta.docs.description,
fixable: rule.meta.fixable
? 'fixable'
: rule.meta.docs?.suggestion
: rule.meta.docs.suggestion
? 'suggest'
: false,
}),
Expand Down