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

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

Merged
merged 7 commits into from Dec 24, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
26 changes: 21 additions & 5 deletions .eslintrc.js
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,33 @@ 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',
// plugins don't have any generated types, so it's safe to use
'@typescript-eslint/internal/no-typescript-default-import': 'off',
},
},
// 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
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
@@ -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
@@ -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
@@ -0,0 +1,18 @@
{
"name": "@typescript-eslint/eslint-plugin-internal",
"version": "2.12.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.12.0"
},
"devDependencies": {}
}
5 changes: 5 additions & 0 deletions packages/eslint-plugin-internal/src/index.ts
@@ -0,0 +1,5 @@
import rules from './rules';

export = {
rules,
};
7 changes: 7 additions & 0 deletions packages/eslint-plugin-internal/src/rules/index.ts
@@ -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,
};
@@ -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 compat with a wide range of consumers, some of whom don't use `allowSyntheticDefaultImports`, we should
bradzacher marked this conversation as resolved.
Show resolved Hide resolved
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';",
);
},
});
}
},
};
},
});
@@ -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
@@ -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
@@ -0,0 +1 @@
export * from './createRule';
22 changes: 22 additions & 0 deletions packages/eslint-plugin-internal/tests/RuleTester.ts
@@ -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 };
@@ -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,
},
],
}),
});