Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0e2c490
commit 31fa3dc
Showing
9 changed files
with
329 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import type { TSESTree } from "@typescript-eslint/experimental-utils"; | ||
import type { JSONSchema4 } from "json-schema"; | ||
|
||
import { isReadonly, RuleContext, RuleMetaData, RuleResult } from "~/util/rule"; | ||
import { createRule } from "~/util/rule"; | ||
|
||
// The name of this rule. | ||
export const name = "prefer-readonly-type-alias" as const; | ||
|
||
const enum RequiredReadonlyness { | ||
READONLY, | ||
MUTABLE, | ||
EITHER, | ||
} | ||
|
||
// The options this rule can take. | ||
type Options = { | ||
readonly mustBeReadonly: { | ||
readonly pattern: ReadonlyArray<string> | string; | ||
readonly requireOthersToBeMutable: boolean; | ||
}; | ||
readonly mustBeMutable: { | ||
readonly pattern: ReadonlyArray<string> | string; | ||
readonly requireOthersToBeReadonly: boolean; | ||
}; | ||
}; | ||
|
||
// The schema for the rule options. | ||
const schema: JSONSchema4 = [ | ||
{ | ||
type: "object", | ||
properties: { | ||
mustBeReadonly: { | ||
type: "object", | ||
properties: { | ||
pattern: { | ||
type: ["string", "array"], | ||
items: { | ||
type: "string", | ||
}, | ||
}, | ||
requireOthersToBeMutable: { | ||
type: "boolean", | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
mustBeMutable: { | ||
type: "object", | ||
properties: { | ||
pattern: { | ||
type: ["string", "array"], | ||
items: { | ||
type: "string", | ||
}, | ||
}, | ||
requireOthersToBeReadonly: { | ||
type: "boolean", | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
]; | ||
|
||
// The default options for the rule. | ||
const defaultOptions: Options = { | ||
mustBeReadonly: { | ||
pattern: "^Readonly", | ||
requireOthersToBeMutable: false, | ||
}, | ||
mustBeMutable: { | ||
pattern: "^Mutable", | ||
requireOthersToBeReadonly: true, | ||
}, | ||
}; | ||
|
||
// The possible error messages. | ||
const errorMessages = { | ||
mutable: "Mutable types should not be fully readonly.", | ||
readonly: "Readonly types should not be mutable at all.", | ||
mutableReadonly: | ||
"Configuration error - this type must be marked as both readonly and mutable.", | ||
needExplicitMarking: | ||
"Type must be explicity marked as either readonly or mutable.", | ||
} as const; | ||
|
||
// The meta data for this rule. | ||
const meta: RuleMetaData<keyof typeof errorMessages> = { | ||
type: "suggestion", | ||
docs: { | ||
description: "Prefer readonly type alias over mutable one.", | ||
category: "Best Practices", | ||
recommended: "error", | ||
}, | ||
messages: errorMessages, | ||
fixable: "code", | ||
schema, | ||
}; | ||
|
||
/** | ||
* Check if the given TypeReference violates this rule. | ||
*/ | ||
function checkTypeAliasDeclaration( | ||
node: TSESTree.TSTypeAliasDeclaration, | ||
context: RuleContext<keyof typeof errorMessages, Options>, | ||
options: Options | ||
): RuleResult<keyof typeof errorMessages, Options> { | ||
const mustBeReadonlyPatterns = ( | ||
Array.isArray(options.mustBeReadonly.pattern) | ||
? options.mustBeReadonly.pattern | ||
: [options.mustBeReadonly.pattern] | ||
).map((pattern) => new RegExp(pattern, "u")); | ||
|
||
const mustBeMutablePatterns = ( | ||
Array.isArray(options.mustBeMutable.pattern) | ||
? options.mustBeMutable.pattern | ||
: [options.mustBeMutable.pattern] | ||
).map((pattern) => new RegExp(pattern, "u")); | ||
|
||
const patternStatesReadonly = mustBeReadonlyPatterns.some((pattern) => | ||
pattern.test(node.id.name) | ||
); | ||
const patternStatesMutable = mustBeMutablePatterns.some((pattern) => | ||
pattern.test(node.id.name) | ||
); | ||
|
||
if (patternStatesReadonly && patternStatesMutable) { | ||
return { | ||
context, | ||
descriptors: [ | ||
{ | ||
node: node.id, | ||
messageId: "mutableReadonly", | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
if ( | ||
!patternStatesReadonly && | ||
!patternStatesMutable && | ||
options.mustBeReadonly.requireOthersToBeMutable && | ||
options.mustBeMutable.requireOthersToBeReadonly | ||
) { | ||
return { | ||
context, | ||
descriptors: [ | ||
{ | ||
node: node.id, | ||
messageId: "needExplicitMarking", | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
const requiredReadonlyness = | ||
patternStatesReadonly || | ||
(!patternStatesMutable && options.mustBeMutable.requireOthersToBeReadonly) | ||
? RequiredReadonlyness.READONLY | ||
: patternStatesMutable || | ||
(!patternStatesReadonly && | ||
options.mustBeReadonly.requireOthersToBeMutable) | ||
? RequiredReadonlyness.MUTABLE | ||
: RequiredReadonlyness.EITHER; | ||
|
||
return checkRequiredReadonlyness( | ||
node, | ||
context, | ||
options, | ||
requiredReadonlyness | ||
); | ||
} | ||
|
||
function checkRequiredReadonlyness( | ||
node: TSESTree.TSTypeAliasDeclaration, | ||
context: RuleContext<keyof typeof errorMessages, Options>, | ||
options: Options, | ||
requiredReadonlyness: RequiredReadonlyness | ||
): RuleResult<keyof typeof errorMessages, Options> { | ||
if (requiredReadonlyness !== RequiredReadonlyness.EITHER) { | ||
const readonly = isReadonly(node.typeAnnotation, context); | ||
|
||
if (readonly && requiredReadonlyness === RequiredReadonlyness.MUTABLE) { | ||
return { | ||
context, | ||
descriptors: [ | ||
{ | ||
node: node.id, | ||
messageId: "readonly", | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
if (!readonly && requiredReadonlyness === RequiredReadonlyness.READONLY) { | ||
return { | ||
context, | ||
descriptors: [ | ||
{ | ||
node: node.id, | ||
messageId: "mutable", | ||
}, | ||
], | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
context, | ||
descriptors: [], | ||
}; | ||
} | ||
|
||
// Create the rule. | ||
export const rule = createRule<keyof typeof errorMessages, Options>( | ||
name, | ||
meta, | ||
defaultOptions, | ||
{ | ||
TSTypeAliasDeclaration: checkTypeAliasDeclaration, | ||
} | ||
); |
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,6 @@ | ||
import { name, rule } from "~/rules/prefer-readonly-type-alias"; | ||
import { testUsing } from "~/tests/helpers/testers"; | ||
|
||
import tsTests from "./ts"; | ||
|
||
testUsing.typescript(name, rule, tsTests); |
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,7 @@ | ||
import invalid from "./invalid"; | ||
import valid from "./valid"; | ||
|
||
export default { | ||
valid, | ||
invalid, | ||
}; |
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,48 @@ | ||
import dedent from "dedent"; | ||
|
||
import type { InvalidTestCase } from "~/tests/helpers/util"; | ||
|
||
const tests: ReadonlyArray<InvalidTestCase> = [ | ||
// Readonly types should not be mutable. | ||
{ | ||
code: dedent` | ||
type MyType = { | ||
a: string; | ||
};`, | ||
optionsSet: [[]], | ||
// output: dedent` | ||
// type MyType = { | ||
// readonly a: string; | ||
// };`, | ||
errors: [ | ||
{ | ||
messageId: "mutable", | ||
type: "Identifier", | ||
line: 1, | ||
column: 6, | ||
}, | ||
], | ||
}, | ||
// Mutable types should not be readonly. | ||
{ | ||
code: dedent` | ||
type MutableMyType = { | ||
readonly a: string; | ||
};`, | ||
optionsSet: [[]], | ||
// output: dedent` | ||
// type MutableMyType = { | ||
// a: string; | ||
// };`, | ||
errors: [ | ||
{ | ||
messageId: "readonly", | ||
type: "Identifier", | ||
line: 1, | ||
column: 6, | ||
}, | ||
], | ||
}, | ||
]; | ||
|
||
export default tests; |
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,24 @@ | ||
import dedent from "dedent"; | ||
|
||
import type { ValidTestCase } from "~/tests/helpers/util"; | ||
|
||
const tests: ReadonlyArray<ValidTestCase> = [ | ||
// Readonly types should be readonly. | ||
{ | ||
code: dedent` | ||
type MyType = { | ||
readonly a: string; | ||
};`, | ||
optionsSet: [[]], | ||
}, | ||
{ | ||
code: dedent` | ||
type MutableMyType = { | ||
a: string; | ||
}; | ||
type MyType = Readonly<MutableMyType>;`, | ||
optionsSet: [[]], | ||
}, | ||
]; | ||
|
||
export default tests; |