Skip to content

feat: add internal eslint plugin for repo-specific lint rules #1373

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

Merged
merged 7 commits into from
Dec 24, 2019
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
24 changes: 19 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
'jest',
'import',
'eslint-comments',
'@typescript-eslint/internal',
],
env: {
es6: true,
Expand Down Expand Up @@ -117,6 +118,11 @@ module.exports = {
'import/no-self-import': 'error',
// Require modules with a single export to use a default export
'import/prefer-default-export': 'off', // we want everything to be named

//
// Internal repo rules
//
'@typescript-eslint/internal/no-typescript-default-import': 'error',
},
parserOptions: {
sourceType: 'module',
Expand All @@ -127,8 +133,10 @@ module.exports = {
tsconfigRootDir: __dirname,
},
overrides: [
// all test files
{
files: [
'packages/eslint-plugin-internal/tests/**/*.test.ts',
'packages/eslint-plugin-tslint/tests/**/*.ts',
'packages/eslint-plugin/tests/**/*.test.ts',
'packages/parser/tests/**/*.ts',
Expand All @@ -138,6 +146,7 @@ module.exports = {
'jest/globals': true,
},
rules: {
'eslint-plugin/no-identical-tests': 'error',
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-alias-methods': 'error',
Expand All @@ -152,26 +161,31 @@ module.exports = {
'jest/valid-expect': 'error',
},
},
// plugin source files
{
files: [
'packages/eslint-plugin/tests/**/*.test.ts',
'packages/eslint-plugin-tslint/tests/**/*.spec.ts',
'packages/eslint-plugin-internal/**/*.ts',
'packages/eslint-plugin-tslint/**/*.ts',
'packages/eslint-plugin/**/*.ts',
],
rules: {
'eslint-plugin/no-identical-tests': 'error',
'@typescript-eslint/internal/no-typescript-estree-import': 'error',
},
},
// rule source files
{
files: [
'packages/eslint-plugin/src/rules/**/*.ts',
'packages/eslint-plugin/src/configs/**/*.ts',
'packages/eslint-plugin-internal/src/rules/**/*.ts',
'packages/eslint-plugin-tslint/src/rules/**/*.ts',
'packages/eslint-plugin/src/configs/**/*.ts',
'packages/eslint-plugin/src/rules/**/*.ts',
],
rules: {
// specifically for rules - default exports makes the tooling easier
'import/no-default-export': 'off',
},
},
// tools and tests
{
files: ['**/tools/**/*.ts', '**/tests/**/*.ts'],
rules: {
Expand Down
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Jest Test Current eslint-plugin-internal Rule",
"cwd": "${workspaceFolder}/packages/eslint-plugin-internal/",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"args": [
"--runInBand",
"--no-coverage",
// needs the '' around it so that the () are properly handled
"'tests/(.+/)?${fileBasenameNoExtension}'"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
Expand Down
5 changes: 5 additions & 0 deletions packages/eslint-plugin-internal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# `eslint-plugin-internal`

This is just a collection of internal lint rules to help enforce some guidelines specific to this repository.

These are not intended to be used externally.
13 changes: 13 additions & 0 deletions packages/eslint-plugin-internal/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: './tests/.+\\.test\\.ts$',
collectCoverage: false,
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coverageReporters: ['text-summary', 'lcov'],
};
18 changes: 18 additions & 0 deletions packages/eslint-plugin-internal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@typescript-eslint/eslint-plugin-internal",
"version": "2.13.0",
"private": true,
"main": "dist/index.js",
"scripts": {
"build": "tsc -b tsconfig.build.json",
"clean": "tsc -b tsconfig.build.json --clean",
"format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore",
"lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'",
"test": "jest --coverage",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "2.13.0"
},
"devDependencies": {}
}
5 changes: 5 additions & 0 deletions packages/eslint-plugin-internal/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import rules from './rules';

export = {
rules,
};
7 changes: 7 additions & 0 deletions packages/eslint-plugin-internal/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import noTypescriptDefaultImport from './no-typescript-default-import';
import noTypescriptEstreeImport from './no-typescript-estree-import';

export default {
'no-typescript-default-import': noTypescriptDefaultImport,
'no-typescript-estree-import': noTypescriptEstreeImport,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import { createRule } from '../util';

/*
We have `allowSyntheticDefaultImports` turned on in this project, so there are two problems that arise:
- TypeScript's auto import will suggest `import ts = require('typescript');` if you type `ts`
- VSCode's suggestion feature will suggest changing `import * as ts from 'typescript'` to `import ts from 'typescript'`

In order to keep compatibility with a wide range of consumers, some of whom don't use `allowSyntheticDefaultImports`, we should
always use either:
- `import * as ts from 'typescript';`
- `import { SyntaxKind } from 'typescript';`
*/

export default createRule({
name: 'no-typescript-default-import',
meta: {
type: 'problem',
docs: {
description:
"Enforces that packages rules don't do `import ts from 'typescript';`",
category: 'Possible Errors',
recommended: 'error',
},
fixable: 'code',
schema: [],
messages: {
noTSDefaultImport: [
"Do not use the default import for typescript. Doing so will cause the package's type definitions to do the same.",
"This causes errors for consumers if they don't use the allowSyntheticDefaultImports compiler option.",
].join('\n'),
},
},
defaultOptions: [],
create(context) {
return {
'ImportDeclaration > ImportDefaultSpecifier'(
node: TSESTree.ImportDefaultSpecifier,
): void {
const importStatement = node.parent as TSESTree.ImportDeclaration;
if (importStatement.source.value === 'typescript') {
context.report({
node,
messageId: 'noTSDefaultImport',
fix(fixer) {
if (importStatement.specifiers.length === 1) {
return fixer.replaceText(node, '* as ts');
}

return null;
},
});
}
},
'TSImportEqualsDeclaration > TSExternalModuleReference'(
node: TSESTree.TSExternalModuleReference,
): void {
const parent = node.parent as TSESTree.TSImportEqualsDeclaration;
if (
node.expression.type === AST_NODE_TYPES.Literal &&
node.expression.value === 'typescript'
) {
context.report({
node,
messageId: 'noTSDefaultImport',
fix(fixer) {
return fixer.replaceText(
parent,
"import * as ts from 'typescript';",
);
},
});
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createRule } from '../util';

const TSESTREE_NAME = '@typescript-eslint/typescript-estree';
const UTILS_NAME = '@typescript-eslint/experimental-utils';

/*
Typescript will not error if people use typescript-estree within eslint-plugin.
This is because it's an indirect dependency.
We don't want people to import it, instead we want them to import from the utils package.
*/

export default createRule({
name: 'no-typescript-estree-import',
meta: {
type: 'problem',
docs: {
description: `Enforces that eslint-plugin rules don't require anything from ${TSESTREE_NAME}`,
category: 'Possible Errors',
recommended: 'error',
},
fixable: 'code',
schema: [],
messages: {
dontImportTSEStree: [
`Don't import from ${TSESTREE_NAME}. Everything you need should be available in ${UTILS_NAME}.`,
`${TSESTREE_NAME} is an indirect dependency of this package, and thus should not be used directly.`,
].join('\n'),
},
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node): void {
if (
typeof node.source.value === 'string' &&
node.source.value.startsWith(TSESTREE_NAME)
) {
context.report({
node,
messageId: 'dontImportTSEStree',
fix(fixer) {
return fixer.replaceTextRange(
[node.source.range[0] + 1, node.source.range[1] - 1],
UTILS_NAME,
);
},
});
}
},
};
},
});
11 changes: 11 additions & 0 deletions packages/eslint-plugin-internal/src/util/createRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ESLintUtils } from '@typescript-eslint/experimental-utils';

// note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder
const version = require('../../package.json').version;

const createRule = ESLintUtils.RuleCreator(
name =>
`https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin-internal/src/rules/${name}.ts`,
);

export { createRule };
1 change: 1 addition & 0 deletions packages/eslint-plugin-internal/src/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createRule';
22 changes: 22 additions & 0 deletions packages/eslint-plugin-internal/tests/RuleTester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils';

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),
});
}
}

export { RuleTester, batchedSingleLineTests };
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import rule from '../../src/rules/no-typescript-default-import';
import { RuleTester, batchedSingleLineTests } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
});

ruleTester.run('no-typescript-default-import', rule, {
valid: [
"import { foo } from 'typescript';",
"import ts from 'nottypescript';",
"import * as foo from 'typescript';",
'import ts = foo;',
"import ts = require('nottypescript');",
],
invalid: batchedSingleLineTests({
code: `
import ts from 'typescript';
import ts, { SyntaxKind } from 'typescript';
import ts = require('typescript');
`,
output: `
import * as ts from 'typescript';
import ts, { SyntaxKind } from 'typescript';
import * as ts from 'typescript';
`,
errors: [
{
messageId: 'noTSDefaultImport',
line: 2,
},
{
messageId: 'noTSDefaultImport',
line: 3,
},
{
messageId: 'noTSDefaultImport',
line: 4,
},
],
}),
});
Loading