Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed Aug 3, 2021
1 parent 0e2c490 commit 31fa3dc
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/configs/all.ts
Expand Up @@ -22,6 +22,7 @@ const config: Linter.Config = {
"functional/no-method-signature": "error",
"functional/no-mixed-type": "error",
"functional/prefer-readonly-type": "error",
"functional/prefer-readonly-type-alias": "error",
"functional/prefer-tacit": ["error", { assumeTypes: false }],
"functional/no-return-void": "error",
},
Expand Down
1 change: 1 addition & 0 deletions src/configs/no-mutations.ts
Expand Up @@ -11,6 +11,7 @@ const config: Linter.Config = {
rules: {
"functional/no-method-signature": "warn",
"functional/prefer-readonly-type": "error",
"functional/prefer-readonly-type-alias": "error",
},
},
],
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Expand Up @@ -13,6 +13,7 @@ import * as noThisExpression from "./no-this-expression";
import * as noThrowStatement from "./no-throw-statement";
import * as noTryStatement from "./no-try-statement";
import * as preferReadonlyTypes from "./prefer-readonly-type";
import * as preferReadonlyTypeAlias from "./prefer-readonly-type-alias";
import * as preferTacit from "./prefer-tacit";

/**
Expand All @@ -34,5 +35,6 @@ export const rules = {
[noThrowStatement.name]: noThrowStatement.rule,
[noTryStatement.name]: noTryStatement.rule,
[preferReadonlyTypes.name]: preferReadonlyTypes.rule,
[preferReadonlyTypeAlias.name]: preferReadonlyTypeAlias.rule,
[preferTacit.name]: preferTacit.rule,
};
225 changes: 225 additions & 0 deletions src/rules/prefer-readonly-type-alias.ts
@@ -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,
}
);
15 changes: 15 additions & 0 deletions src/util/rule.ts
Expand Up @@ -138,6 +138,21 @@ export function getTypeOfNode<Context extends RuleContext<string, BaseOptions>>(
return constrained ?? nodeType;
}

export function isReadonly<Context extends RuleContext<string, BaseOptions>>(
node: TSESTree.Node,
context: Context
): boolean {
const { parserServices } = context;

if (parserServices === undefined || parserServices.program === undefined) {
return false;
}

const checker = parserServices.program.getTypeChecker();
const type = getTypeOfNode(node, context);
return ESLintUtils.isTypeReadonly(checker, type!);
}

/**
* Get the es tree node from the given ts node.
*/
Expand Down
6 changes: 6 additions & 0 deletions tests/rules/prefer-readonly-type-alias/index.test.ts
@@ -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);
7 changes: 7 additions & 0 deletions tests/rules/prefer-readonly-type-alias/ts/index.ts
@@ -0,0 +1,7 @@
import invalid from "./invalid";
import valid from "./valid";

export default {
valid,
invalid,
};
48 changes: 48 additions & 0 deletions tests/rules/prefer-readonly-type-alias/ts/invalid.ts
@@ -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;
24 changes: 24 additions & 0 deletions tests/rules/prefer-readonly-type-alias/ts/valid.ts
@@ -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;

0 comments on commit 31fa3dc

Please sign in to comment.