This commit is contained in:
Alex
2023-11-10 20:29:53 -05:00
parent ee3e7e4203
commit 60b1b8fde3
17 changed files with 539 additions and 128 deletions

View File

@ -7,14 +7,18 @@ const files = readdirSync('./src/rules')
.filter(file => file !== 'index.ts')
.map(file => file.slice(0, -3));
const entryFile = `
const entryFile = /* js */ `
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
${files.map(file => `import ${camelCase(file)} from "./${file}"`).join(';\n')}
${files.map(file => `import ${camelCase(file)} from './${file}';`).join('\n')}
export const rules: Record<string, Rule.RuleModule> = {
${files.map(file => `"${file}": ${camelCase(file)}`).join(',\n ')}
export const rules: Record<
string,
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
> = {
${files.map(file => `'${file}': ${camelCase(file)},`).join('\n ')}
};
`.trim();
writeFileSync('./src/rules/index.ts', entryFile);
writeFileSync('./src/rules/index.ts', entryFile + '\n');

View File

@ -1,9 +1,10 @@
import './redirect';
import type { ESLintConfig } from 'eslint-define-config';
import type { ESLintConfig, Rules } from 'eslint-define-config';
import { typescriptRules } from './presets/typescript';
import { unicornRules } from './presets/unicorn';
import { eslintRules } from './presets/eslint';
import { reactRules } from './presets/react';
import { importRules } from './presets/import';
import { error, warn, off } from './constants';
// @ts-expect-error
const { name } = (0, require)('./package.json');
@ -14,12 +15,39 @@ const unique = <T>(arr: T[]): T[] => [...new Set(arr)];
const ensureArray = <T>(value?: T | T[]): T[] =>
value == null ? [] : Array.isArray(value) ? value : [value];
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
declare module 'eslint-define-config/src/rules/react/no-unknown-property.d.ts' {
export interface NoUnknownPropertyOption {
extends: ('next' | 'emotion')[];
}
}
export interface LocalRuleOptions {
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
'rules/no-import-dot': RuleEntry<unknown>;
/**
* Enforce template literal expressions to be of `string` type
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
*/
'rules/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
}
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
/**
* ESLint Configuration.
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
*/
type Config = Omit<ESLintConfig, 'rules'> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
*/
rules?: RuleOptions;
};
/**
* Returns a ESLint config object.
*
@ -33,16 +61,16 @@ export function extendConfig({
extends: _extends,
overrides,
...rest
}: ESLintConfig = {}): ESLintConfig {
}: Config = {}): ESLintConfig {
const hasReact = plugins?.includes('react');
const hasReactRefresh = plugins?.includes('react-refresh');
const hasUnicorn = plugins?.includes('unicorn');
const hasNext = ensureArray(_extends).some(name => name.includes(':@next/next'));
const result: ESLintConfig = {
const result: Config = {
root: true,
parser: '@typescript-eslint/parser',
plugins: unique(['@typescript-eslint', 'import', ...(plugins ?? [])]),
plugins: unique(['@typescript-eslint', 'import', 'rules', ...(plugins ?? [])]),
env: { node: true, browser: true, es2023: true },
reportUnusedDisableDirectives: true,
parserOptions: {
@ -99,9 +127,7 @@ export function extendConfig({
rules: {
...eslintRules,
...typescriptRules,
'import/export': off,
'import/no-duplicates': error,
'import/order': [error, { groups: ['builtin', 'external'] }],
...importRules,
...(hasReact && {
...reactRules,
'react/no-unknown-property': [
@ -113,6 +139,8 @@ export function extendConfig({
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
}),
...(hasUnicorn && unicornRules),
'rules/no-import-dot': error,
'rules/restrict-template-expressions': error,
...rules,
},
...rest,

8
src/presets/import.ts Normal file
View File

@ -0,0 +1,8 @@
import { error, off } from '../constants';
import { ImportRules } from 'eslint-define-config/src/rules/import';
export const importRules: Partial<ImportRules> = {
'import/export': off,
'import/no-duplicates': error,
'import/order': [error, { groups: ['builtin', 'external'] }],
};

6
src/presets/local.ts Normal file
View File

@ -0,0 +1,6 @@
import { error, off } from '../constants';
import type { CustomRuleOptions } from 'eslint-define-config';
export const importRules: Partial<CustomRuleOptions> = {
'local/no-import-dot': error,
};

View File

@ -33,6 +33,7 @@ export const typescriptRules: Partial<TypeScriptRules> = {
],
'@typescript-eslint/no-use-before-define': off,
'@typescript-eslint/no-var-requires': off,
'@typescript-eslint/restrict-template-expressions': off,
'@typescript-eslint/triple-slash-reference': off,
'@typescript-eslint/unbound-method': off,
};

View File

@ -1,7 +1,13 @@
import type { Rule } from 'eslint';
import type { ESLintUtils } from '@typescript-eslint/utils';
import noImportDot from "./no-import-dot"
import noImportDot from './no-import-dot';
import restrictTemplateExpressions from './restrict-template-expressions';
export const rules: Record<string, Rule.RuleModule> = {
"no-import-dot": noImportDot
};
export const rules: Record<
string,
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
> = {
'no-import-dot': noImportDot,
'restrict-template-expressions': restrictTemplateExpressions,
};

View File

@ -1,19 +1,19 @@
import type { Rule } from "eslint";
import type { Rule } from 'eslint';
const rule: Rule.RuleModule = {
meta: {
type: "problem",
type: 'problem',
docs: {
description:
"Bans import from the specifier '.' and '..' and replaces it with '.+/index'",
category: "Best Practices",
category: 'Best Practices',
recommended: true,
},
fixable: "code",
fixable: 'code',
},
create: context => ({
ImportDeclaration(node) {
if (node.source.value === ".") {
if (node.source.value === '.') {
context.report({
node: node.source,
message:
@ -22,7 +22,7 @@ const rule: Rule.RuleModule = {
return fixer.replaceText(node.source, '"./index"');
},
});
} else if (node.source.value === "..") {
} else if (node.source.value === '..') {
context.report({
node: node.source,
message:

View File

@ -0,0 +1,122 @@
// https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts
import * as ts from 'typescript';
import { ESLintUtils, type TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
import {
getTypeName,
isTypeAnyType,
isTypeFlagSet,
isTypeNeverType,
getConstrainedTypeAtLocation,
} from '@typescript-eslint/type-utils';
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
const createRule = ESLintUtils.RuleCreator(
name => `https://typescript-eslint.io/rules/${name}`,
);
interface Option {
allow: string[];
}
const defaultOption: Option = {
allow: ['any', 'boolean', 'null', 'undefined', 'number', 'RegExp', 'URLSearchParams'],
};
type MessageId = 'invalidType';
export default createRule<Option[], MessageId>({
name: 'restrict-template-expressions',
meta: {
type: 'problem',
docs: {
description: 'Enforce template literal expressions to be of `string` type',
recommended: 'recommended',
requiresTypeChecking: true,
},
messages: {
invalidType: 'Invalid type "{{type}}" of template literal expression.',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
allow: {
type: 'array',
items: {
type: 'string',
},
description: 'Allow specific types',
uniqueItems: true,
},
},
},
],
},
defaultOptions: [defaultOption],
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program!.getTypeChecker();
const allowed = new Set(options.allow);
const { StringLike, NumberLike, BigIntLike, BooleanLike, Null, Undefined } =
ts.TypeFlags;
function isUnderlyingTypePrimitive(type: ts.Type): boolean {
return (
isTypeFlagSet(type, StringLike) ||
(allowed.has('number') && isTypeFlagSet(type, NumberLike | BigIntLike)) ||
(allowed.has('boolean') && isTypeFlagSet(type, BooleanLike)) ||
(allowed.has('any') && isTypeAnyType(type)) ||
allowed.has(getTypeName(checker, type)) ||
(allowed.has('null') && isTypeFlagSet(type, Null)) ||
(allowed.has('undefined') && isTypeFlagSet(type, Undefined)) ||
(allowed.has('never') && isTypeNeverType(type))
);
}
return {
TemplateLiteral(node: TSESTree.TemplateLiteral): void {
// don't check tagged template literals
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
return;
}
for (const expression of node.expressions) {
const expressionType = getConstrainedTypeAtLocation(services, expression);
if (
!isInnerUnionOrIntersectionConformingTo(
expressionType,
isUnderlyingTypePrimitive,
)
) {
context.report({
node: expression,
messageId: 'invalidType',
data: { type: checker.typeToString(expressionType) },
});
}
}
},
};
function isInnerUnionOrIntersectionConformingTo(
type: ts.Type,
predicate: (underlyingType: ts.Type) => boolean,
): boolean {
return rec(type);
function rec(innerType: ts.Type): boolean {
if (innerType.isUnion()) {
return innerType.types.every(rec);
}
if (innerType.isIntersection()) {
return innerType.types.some(rec);
}
return predicate(innerType);
}
}
},
});

15
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
declare module '@typescript-eslint/utils' {
export * from '@typescript-eslint/utils/dist/index';
}
declare module '@typescript-eslint/typescript-estree' {
export * from '@typescript-eslint/typescript-estree/dist/index';
}
declare module '@typescript-eslint/type-utils' {
export * from '@typescript-eslint/type-utils/dist/index';
}
declare module '@typescript-eslint/utils/eslint-utils' {
export * from '@typescript-eslint/utils/dist/eslint-utils';
}
declare module '@typescript-eslint/utils/json-schema' {
export * from '@typescript-eslint/utils/dist/json-schema';
}