-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(type-declaration-immutability): create rule
- Loading branch information
1 parent
01e7eb4
commit 7838fd2
Showing
18 changed files
with
1,177 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
import type { ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; | ||
import { deepmerge } from "deepmerge-ts"; | ||
import { Immutability } from "is-immutable-type"; | ||
import type { JSONSchema4 } from "json-schema"; | ||
import type { ReadonlyDeep } from "type-fest"; | ||
|
||
import type { IgnorePatternOption } from "~/common/ignore-options"; | ||
import { | ||
shouldIgnorePattern, | ||
ignorePatternOptionSchema, | ||
} from "~/common/ignore-options"; | ||
import { getNodeIdentifierTexts } from "~/util/misc"; | ||
import type { ESTypeDeclaration } from "~/util/node-types"; | ||
import type { RuleResult } from "~/util/rule"; | ||
import { getTypeImmutabilityOfNode, createRule } from "~/util/rule"; | ||
import { isReadonlyArray, isTSInterfaceDeclaration } from "~/util/typeguard"; | ||
|
||
/** | ||
* The name of this rule. | ||
*/ | ||
export const name = "type-declaration-immutability" as const; | ||
|
||
/** | ||
* How the actual immutability should be compared to the given immutability. | ||
*/ | ||
export enum RuleEnforcementComparator { | ||
Less = -2, | ||
AtMost = -1, | ||
Exactly = 0, | ||
AtLeast = 1, | ||
More = 2, | ||
} | ||
|
||
/** | ||
* The options this rule can take. | ||
*/ | ||
type Options = ReadonlyDeep< | ||
[ | ||
IgnorePatternOption & { | ||
rules?: Array<{ | ||
identifier: string | string[]; | ||
immutability: Exclude< | ||
Immutability | keyof typeof Immutability, | ||
"Unknown" | ||
>; | ||
comparator?: | ||
| RuleEnforcementComparator | ||
| keyof typeof RuleEnforcementComparator; | ||
}>; | ||
ignoreInterfaces: boolean; | ||
} | ||
] | ||
>; | ||
|
||
/** | ||
* The schema for the rule options. | ||
*/ | ||
const schema: JSONSchema4 = [ | ||
{ | ||
type: "object", | ||
properties: deepmerge(ignorePatternOptionSchema, { | ||
rules: { | ||
type: "array", | ||
items: { | ||
type: "object", | ||
properties: { | ||
identifier: { | ||
type: ["string", "array"], | ||
items: { | ||
type: "string", | ||
}, | ||
}, | ||
immutability: { | ||
type: ["string", "number"], | ||
enum: Object.values(Immutability).filter( | ||
(i) => | ||
i !== Immutability.Unknown && | ||
i !== Immutability[Immutability.Unknown] | ||
), | ||
}, | ||
comparator: { | ||
type: ["string", "number"], | ||
enum: Object.values(RuleEnforcementComparator), | ||
}, | ||
}, | ||
required: ["identifier", "immutability"], | ||
additionalProperties: false, | ||
}, | ||
}, | ||
ignoreInterfaces: { | ||
type: "boolean", | ||
}, | ||
}), | ||
additionalProperties: false, | ||
}, | ||
]; | ||
|
||
/** | ||
* The default options for the rule. | ||
*/ | ||
const defaultOptions: Options = [ | ||
{ | ||
ignoreInterfaces: false, | ||
}, | ||
]; | ||
|
||
/** | ||
* The possible error messages. | ||
*/ | ||
const errorMessages = { | ||
Less: 'This type is declare to have an immutability less than "{{ expected }}" (actual: "{{ actual }}").', | ||
AtLeast: | ||
'This type is declare to have an immutability of at least "{{ expected }}" (actual: "{{ actual }}").', | ||
Exactly: | ||
'This type is declare to have an immutability of exactly "{{ expected }}" (actual: "{{ actual }}").', | ||
AtMost: | ||
'This type is declare to have an immutability of at most "{{ expected }}" (actual: "{{ actual }}").', | ||
More: 'This type is declare to have an immutability more than "{{ expected }}" (actual: "{{ actual }}").', | ||
} as const; | ||
|
||
/** | ||
* The meta data for this rule. | ||
*/ | ||
const meta: ESLintUtils.NamedCreateRuleMeta<keyof typeof errorMessages> = { | ||
type: "suggestion", | ||
docs: { | ||
description: "Enforce the immutability of types based on patterns.", | ||
recommended: "error", | ||
}, | ||
messages: errorMessages, | ||
schema, | ||
}; | ||
|
||
/** | ||
* A rule given by the user after being upgraded. | ||
*/ | ||
export type ImmutabilityRule = { | ||
identifiers: ReadonlyArray<RegExp>; | ||
immutability: Immutability; | ||
comparator: RuleEnforcementComparator; | ||
}; | ||
|
||
/** | ||
* Get the default immutability rules. | ||
*/ | ||
function getDefaultImmutabilityRules(): ImmutabilityRule[] { | ||
return [ | ||
{ | ||
identifiers: [/^I?Immutable.+/u], | ||
immutability: Immutability.Immutable, | ||
comparator: RuleEnforcementComparator.AtLeast, | ||
}, | ||
{ | ||
identifiers: [/^I?ReadonlyDeep.+/u], | ||
immutability: Immutability.ReadonlyDeep, | ||
comparator: RuleEnforcementComparator.AtLeast, | ||
}, | ||
{ | ||
identifiers: [/^I?Readonly.+/u], | ||
immutability: Immutability.ReadonlyShallow, | ||
comparator: RuleEnforcementComparator.AtLeast, | ||
}, | ||
{ | ||
identifiers: [/^I?Mutable.+/u], | ||
immutability: Immutability.Mutable, | ||
comparator: RuleEnforcementComparator.AtMost, | ||
}, | ||
]; | ||
} | ||
|
||
/** | ||
* Get all the rules that were given and upgrade them. | ||
*/ | ||
function getRules(options: Options): ImmutabilityRule[] { | ||
const [optionsObject] = options; | ||
const { rules: rulesOptions } = optionsObject; | ||
|
||
if (rulesOptions === undefined) { | ||
return getDefaultImmutabilityRules(); | ||
} | ||
|
||
return rulesOptions.map((rule): ImmutabilityRule => { | ||
const identifiers = isReadonlyArray(rule.identifier) | ||
? rule.identifier.map((id) => new RegExp(id, "u")) | ||
: [new RegExp(rule.identifier, "u")]; | ||
|
||
const immutability = | ||
typeof rule.immutability === "string" | ||
? Immutability[rule.immutability] | ||
: rule.immutability; | ||
|
||
const comparator = | ||
rule.comparator === undefined | ||
? RuleEnforcementComparator.AtLeast | ||
: typeof rule.comparator === "string" | ||
? RuleEnforcementComparator[rule.comparator] | ||
: rule.comparator; | ||
|
||
return { | ||
identifiers, | ||
immutability, | ||
comparator, | ||
}; | ||
}); | ||
} | ||
|
||
/** | ||
* Find the first rule to apply to the given node. | ||
*/ | ||
export function getRuleToApply( | ||
node: ReadonlyDeep<TSESTree.Node>, | ||
context: ReadonlyDeep< | ||
TSESLint.RuleContext<keyof typeof errorMessages, Options> | ||
>, | ||
options: Options | ||
): ImmutabilityRule | undefined { | ||
const rules = getRules(options); | ||
if (rules.length === 0) { | ||
return undefined; | ||
} | ||
|
||
const texts = getNodeIdentifierTexts(node, context); | ||
|
||
if (texts.length === 0) { | ||
return undefined; | ||
} | ||
|
||
return rules.find((rule) => | ||
rule.identifiers.some((pattern) => texts.some((text) => pattern.test(text))) | ||
); | ||
} | ||
|
||
/** | ||
* Compare the actual immutability to the expected immutability. | ||
*/ | ||
export function compareImmutability( | ||
rule: ReadonlyDeep<ImmutabilityRule>, | ||
actual: Immutability | ||
) { | ||
switch (rule.comparator) { | ||
case RuleEnforcementComparator.Less: | ||
return actual < rule.immutability; | ||
case RuleEnforcementComparator.AtMost: | ||
return actual <= rule.immutability; | ||
case RuleEnforcementComparator.Exactly: | ||
return actual === rule.immutability; | ||
case RuleEnforcementComparator.AtLeast: | ||
return actual >= rule.immutability; | ||
case RuleEnforcementComparator.More: | ||
return actual > rule.immutability; | ||
} | ||
} | ||
|
||
/** | ||
* Get the results. | ||
*/ | ||
function getResults( | ||
node: ReadonlyDeep<ESTypeDeclaration>, | ||
context: ReadonlyDeep< | ||
TSESLint.RuleContext<keyof typeof errorMessages, Options> | ||
>, | ||
rule: ImmutabilityRule, | ||
immutability: Immutability | ||
): RuleResult<keyof typeof errorMessages, Options> { | ||
const valid = compareImmutability(rule, immutability); | ||
if (valid) { | ||
return { | ||
context, | ||
descriptors: [], | ||
}; | ||
} | ||
|
||
return { | ||
context, | ||
descriptors: [ | ||
{ | ||
node: node.id, | ||
messageId: RuleEnforcementComparator[ | ||
rule.comparator | ||
] as keyof typeof RuleEnforcementComparator, | ||
data: { | ||
actual: Immutability[immutability], | ||
expected: Immutability[rule.immutability], | ||
}, | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
/** | ||
* Check if the given Interface or Type Alias violates this rule. | ||
*/ | ||
function checkTypeDeclaration( | ||
node: ReadonlyDeep<ESTypeDeclaration>, | ||
context: ReadonlyDeep< | ||
TSESLint.RuleContext<keyof typeof errorMessages, Options> | ||
>, | ||
options: Options | ||
): RuleResult<keyof typeof errorMessages, Options> { | ||
const [optionsObject] = options; | ||
const { ignoreInterfaces } = optionsObject; | ||
if ( | ||
shouldIgnorePattern(node, context, optionsObject) || | ||
(ignoreInterfaces && isTSInterfaceDeclaration(node)) | ||
) { | ||
return { | ||
context, | ||
descriptors: [], | ||
}; | ||
} | ||
|
||
const rule = getRuleToApply(node, context, options); | ||
if (rule === undefined) { | ||
return { | ||
context, | ||
descriptors: [], | ||
}; | ||
} | ||
|
||
const immutability = getTypeImmutabilityOfNode(node, context); | ||
|
||
return getResults(node, context, rule, immutability); | ||
} | ||
|
||
// Create the rule. | ||
export const rule = createRule<keyof typeof errorMessages, Options>( | ||
name, | ||
meta, | ||
defaultOptions, | ||
{ | ||
TSTypeAliasDeclaration: checkTypeDeclaration, | ||
TSInterfaceDeclaration: checkTypeDeclaration, | ||
} | ||
); |
Oops, something went wrong.