Skip to content

Commit

Permalink
feat: types with intellisense (eslint-types#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimava committed Mar 31, 2023
1 parent 936119d commit 5755d06
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 150 deletions.
128 changes: 111 additions & 17 deletions scripts/generate-rule-files/src/rule-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,19 @@ export class RuleFile {
.output();
}

private readonly ruleSchemas: {
Setting?: string;
Config?: string;
Option?: string;
Partial: string[];
} = { Partial: [] };

/**
* Generate a type from a JSON schema and append it to the file content.
*/
private async appendJsonSchemaType(
schema: JSONSchema4,
comment: string,
comment: 'Setting' | 'Config' | 'Option',
): Promise<void> {
const type: string = await generateTypeFromSchema(
schema,
Expand All @@ -109,6 +116,12 @@ export class RuleFile {
const jsdoc: string = JsDocBuilder.build().add(`${comment}.`).output();
this.content += `\n${jsdoc}`;
this.content += `\n${type}\n`;
const fullSchema: string = type
.replace(/export (type|interface) \w+\s*(=\s*)?/, '')
.trim();
const [mainSchema, ...parts] = fullSchema.split('\nexport ');
this.ruleSchemas[comment] = mainSchema;
this.ruleSchemas.Partial.push(...parts);
}

/**
Expand Down Expand Up @@ -162,23 +175,40 @@ export class RuleFile {
this.content += `export type ${ruleName}RuleConfig = RuleConfig<${genericContent}>;\n\n`;
}

/**
* Append the final rule interface to the file content.
*/
private appendRule(): void {
const ruleName: string = this.ruleNamePascalCase;
private get prefixedRuleName(): string {
const { prefix, name } = this.plugin;
let rulePrefix: string = (prefix ?? kebabCase(name)) + '/';
if (name === 'Eslint') {
rulePrefix = '';
}
return `${rulePrefix}${this.ruleName}`;
}

this.content += this.generateTypeJsDoc() + '\n';
/**
* Append the final rule interface to the file content.
*/
private appendRule(): void {
const ruleName: string = this.ruleNamePascalCase;

this.content += `export interface ${ruleName}Rule {`;
this.content += `${this.generateTypeJsDoc()}\n`;
this.content += `'${rulePrefix}${this.ruleName}': ${ruleName}RuleConfig;`;
this.content += '}';
const nestedDepth: number = this.ruleName.split('/').length;
const importsRootPath: string = '../'.repeat(nestedDepth);

const ruleSchema: string[] = [
'RuleLevel',
this.ruleSchemas.Option,
this.ruleSchemas.Config,
this.ruleSchemas.Setting,
].filter((e): e is string => !!e);

this.content += `
import type { Rule } from '${importsRootPath}rule-config';
import type { RuleLevel } from '${importsRootPath}rule-severity';
export type ${ruleName}Rule = {
${this.generateTypeJsDoc()}
'${this.prefixedRuleName}': Rule<[${ruleSchema.join(',\n')}]>
}
${this.ruleSchemas.Partial.join('\n')}
`;
}

/**
Expand All @@ -195,18 +225,16 @@ export class RuleFile {
* Generate a file with the rule typings.
*/
public async generate(): Promise<string> {
this.appendRuleConfigImport();
// for this.ruleSchemas side effects
await this.appendRuleSchemaTypes();

if (this.mainSchema) {
this.appendRuleOptions();
}

this.appendRuleConfig();
this.content = '';
this.appendRule();

this.content = format(this.content);

this.applyContextualFixes();

return this.content;
}

Expand All @@ -218,4 +246,70 @@ export class RuleFile {

writeFileSync(this.rulePath, this.content);
}

/**
* Apply handcrafted hard to generalize fixes
*/
public applyContextualFixes(): void {
if (this.prefixedRuleName === '@graphql-eslint/naming-convention') {
// boolean props incompatible with Record
// boolean props incompatible with
this.content = this.content.replace(
`
allowLeadingUnderscore?: boolean;
allowTrailingUnderscore?: boolean;
/**
*/
[k: string]: AsString | AsObject;`,
`
allowLeadingUnderscore?: boolean;
allowTrailingUnderscore?: boolean;
} & {
/**
*/
[k: string]: AsString | AsObject;`,
);
}
if (this.prefixedRuleName === 'node/file-extension-in-import') {
this.content = this.content.replace(
`
tryExtensions?: string[];
[k: string]: 'always' | 'never';`,
`
tryExtensions?: string[];
[ext: \`.\${string}\`]: 'always' | 'never';`,
);
}
if (this.prefixedRuleName === 'padding-line-between-statements') {
this.content = this.content.replace(
`
'padding-line-between-statements': Rule<
[RuleLevel, 'any' | 'never' | 'always']
>;
};`,
`
'padding-line-between-statements': Rule<[RuleLevel, ...StatementType[]]>;
};
type PaddingType = 'any' | 'never' | 'always';`,
);
}
if (
this.prefixedRuleName ===
'@typescript-eslint/padding-line-between-statements'
) {
this.content = this.content.replace(
`
'@typescript-eslint/padding-line-between-statements': Rule<
[RuleLevel, 'any' | 'never' | 'always']
>;
};`,
`
'@typescript-eslint/padding-line-between-statements': Rule<
[RuleLevel, ...StatementType[]]
>;
};
type PaddingType = 'any' | 'never' | 'always';`,
);
}
}
}
14 changes: 6 additions & 8 deletions src/rules/rule-config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ export type RuleLevelAndOptions<Options extends any[] = any[]> = Prepend<
RuleLevel
>;

export type RuleEntry<Options extends any[] = any[]> =
export type Rule<Options extends [RuleLevel, ...any[]]> = //
Options extends [RuleLevel, ...infer Options]
? RuleLevelAndOptions<Options>
: never;

export type RuleConfig<Options extends any[] = any[]> =
| RuleLevel
| RuleLevelAndOptions<Options>;

/**
* Rule configuration.
*
* @alias RuleEntry
*/
export type RuleConfig<Options extends any[] = any[]> = RuleEntry<Options>;
152 changes: 27 additions & 125 deletions tests/__snapshots__/generate-rule-files.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,162 +1,64 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Rule File > Main schema 1`] = `
"import type { RuleConfig } from '../rule-config';
/**
* Option.
*/
export type MyRuleOption = number;
/**
* Options.
*/
export type MyRuleOptions = [MyRuleOption?];
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export type MyRuleRuleConfig = RuleConfig<MyRuleOptions>;
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export interface MyRuleRule {
"import type { Rule } from '../rule-config';
import type { RuleLevel } from '../rule-severity';
export type MyRuleRule = {
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
'my-plugin/my-rule': MyRuleRuleConfig;
}
'my-plugin/my-rule': Rule<[RuleLevel, number]>;
};
"
`;

exports[`Rule File > Object schema 1`] = `
"import type { RuleConfig } from '../rule-config';
/**
* Option.
*/
export interface MyRuleOption {
[k: string]: any;
}
/**
* Options.
*/
export type MyRuleOptions = MyRuleOption;
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export type MyRuleRuleConfig = RuleConfig<MyRuleOptions>;
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export interface MyRuleRule {
"import type { Rule } from '../rule-config';
import type { RuleLevel } from '../rule-severity';
export type MyRuleRule = {
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
'my-plugin/my-rule': MyRuleRuleConfig;
}
'my-plugin/my-rule': Rule<
[
RuleLevel,
{
[k: string]: any;
},
]
>;
};
"
`;

exports[`Rule File > Side schemas 1`] = `
"import type { RuleConfig } from '../rule-config';
/**
* Config.
*/
export type MyRuleConfig = string;
/**
* Option.
*/
export type MyRuleOption = number;
/**
* Options.
*/
export type MyRuleOptions = [MyRuleOption?, MyRuleConfig?];
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export type MyRuleRuleConfig = RuleConfig<MyRuleOptions>;
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export interface MyRuleRule {
"import type { Rule } from '../rule-config';
import type { RuleLevel } from '../rule-severity';
export type MyRuleRule = {
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
'my-plugin/my-rule': MyRuleRuleConfig;
}
'my-plugin/my-rule': Rule<[RuleLevel, number, string]>;
};
"
`;

exports[`Rule File > Third schemas 1`] = `
"import type { RuleConfig } from '../rule-config';
/**
* Setting.
*/
export type MyRuleSetting = boolean;
/**
* Config.
*/
export type MyRuleConfig = string;
/**
* Option.
*/
export type MyRuleOption = number;
/**
* Options.
*/
export type MyRuleOptions = [MyRuleOption?, MyRuleConfig?, MyRuleSetting?];
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export type MyRuleRuleConfig = RuleConfig<MyRuleOptions>;
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
export interface MyRuleRule {
"import type { Rule } from '../rule-config';
import type { RuleLevel } from '../rule-severity';
export type MyRuleRule = {
/**
* My rule description.
*
* @see [my-rule](http:https://test.com/my-rule)
*/
'my-plugin/my-rule': MyRuleRuleConfig;
}
'my-plugin/my-rule': Rule<[RuleLevel, number, string, boolean]>;
};
"
`;

0 comments on commit 5755d06

Please sign in to comment.