eslint-plugin-functional icon indicating copy to clipboard operation
eslint-plugin-functional copied to clipboard

v5.0.0

Open RebeccaStevens opened this issue 3 years ago • 1 comments
trafficstars

Install

npm i -D eslint-plugin-functional@next

yarn add -D eslint-plugin-functional@next

Features

  • [x] Rework of readonly type rules (#472).
  • [x] New ruleset (strict) (#483).
  • [x] Improvements to rulesets (#482 and #483).
  • [ ] Documentation updates.

New Rules

Deprecated Rules

  • prefer-readonly-type

Breaking Changes

  • no-method-signature renamed to prefer-property-signatures and move to stylistic ruleset.
  • prefer-readonly-type remove from all rulesets.
  • reduce strictness of the recommended ruleset.
  • external-recommended ruleset has been split into vanilla and typescript variants
  • remove @typescript-eslint/prefer-readonly-parameter-types from list of recommended external rules
  • Minimum support version of Node is now 16.10.
  • Minimum support version of TypeScript is now 4.0.

RebeccaStevens avatar Sep 19 '22 08:09 RebeccaStevens

Codecov Report

Merging #480 (9bc0752) into main (35142ba) will decrease coverage by 3.63%. The diff coverage is 87.34%.

@@            Coverage Diff             @@
##             main     #480      +/-   ##
==========================================
- Coverage   93.82%   90.20%   -3.63%     
==========================================
  Files          34       46      +12     
  Lines         794     1215     +421     
  Branches      201      319     +118     
==========================================
+ Hits          745     1096     +351     
- Misses         24       51      +27     
- Partials       25       68      +43     
Flag Coverage Δ
4.0.2 90.12% <87.34%> (-3.58%) :arrow_down:
JS 89.95% <87.02%> (-3.50%) :arrow_down:
latest 89.95% <87.02%> (-3.50%) :arrow_down:
next 89.95% <87.02%> (-3.50%) :arrow_down:

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/utils/conditional-imports/typescript.ts 75.00% <ø> (ø)
src/configs/off.ts 66.66% <50.00%> (+4.16%) :arrow_up:
src/utils/merge-configs.ts 53.84% <53.84%> (ø)
src/utils/tsutils.ts 54.54% <54.54%> (ø)
src/utils/rule.ts 75.32% <75.32%> (ø)
src/rules/type-declaration-immutability.ts 78.87% <78.87%> (ø)
src/settings/immutability.ts 79.16% <79.16%> (ø)
src/rules/readonly-type.ts 86.04% <86.04%> (ø)
src/rules/prefer-immutable-types.ts 86.88% <86.88%> (ø)
src/rules/no-conditional-statements.ts 88.73% <87.50%> (ø)
... and 40 more

:mega: We’re building smart automated test selection to slash your CI/CD build times. Learn more

codecov[bot] avatar Sep 19 '22 10:09 codecov[bot]

We have quite many projects relying mainly on prefer-readonly-type to keep things immutable. Since it will be deprecated with 5.0 I'm mostly concerned about the migration path for these projects. My thinking was that I should had time to document a migration path to get the same level of immutability using the new rules but unfortunately I have not had time to do so yet.

Perhaps one way would be some simple comparison table that references prefer-readonly-type and the replacement.

jonaskello avatar Oct 02 '22 09:10 jonaskello

For reference the rules we use are in this repo and more specifically here regarding the prefer-readonly-type rule. The options we use are { allowLocalMutation: true, ignorePattern: "^[mM]utable" }.

EDIT: To elaborate a bit on why we have these settings. We want to keep all "application code" totally immutable and side-effect free. Since (unfortunately) almost every 3rd party package does side-effect, this means we do not have any interaction with 3rd party libraries in "application code". This means combability with 3rd party calls scenario is not a problem for us at all. We do all side-effect and 3rd party calls in a thin "imperative shell", which is a separate package where we can disable the linting.

So for us the perfect solution would be if tsc supported a switch to treat all data as immutable, something like tsc --immutable. In this mode there would be no need for readonly modifer, instead in this mode tsc would support a mut modifier for explicit mutability similar to Rust, F# and other language. However since that probably never is going to happen, the next best thing is to put readonly modifiers everywhere and instead of mut modifier, have a naming convention like leading mut in the name, hence the ignorePattern setting we use.

The reason we have allowLocalMutation set to true is purely for performance. Javascript does not have persistent data types so mutation is really needed for the implementation details in functions.

jonaskello avatar Oct 02 '22 09:10 jonaskello

I think the main job of prefer-readonly-type is basically to ensure that readonly modifier (or equivalent type) is used in all places where typescript supports them. With the new rules I guess this job would be split between other rules so perhaps that could be put in a table in order to find the migration path. This is a incomplete example of that:

Where typescript supports readonly modifier/types:

Type Typescript readonly suppport
Array ReadonlyArray<T> or readonly modifier
Tuple readonly modifier
Object readonly modifier
Set ReadonlySet<T>
Map ReadonlyMap<T>
class member readonly modifier
type member readonly modifier
interface member readonly modifier
more types? ??

Before, prefer-readonly-type would enforce all above, everywhere in the codebase, with location exceptions specified as options. Instead of options for locations, there are now multiple rules. The table below lists which new rule can enforce readonly modifier/types depending on location in the code:

Location New rule
global variable ??
local variable ??
in global type/interface decl type-declaration-immutability, todo check if there is a global/local option?
in local type/interface decl type-declaration-immutability, todo check if there is a global/local option?
function param prefer-immutable-parameter-types
function return-type ??
more locations? ??

jonaskello avatar Oct 02 '22 09:10 jonaskello

Here's how the new setup will work:

const globalVar: { foo: string } = ...; // No error as this doesn't get checked.
type MutableFoo = { foo: string }; // No error as declared as mutable
const globalVar: MutableFoo = ...; // No error
type Foo = { foo: string };  // Error as not immutable
const globalVar: Foo = ...;  // No error.

With the new rules, there is currently no way to check inline type declarations. I guess a new rule should be added to check such declarations but it's not a rule I would use myself as I tend to alias everything.

RebeccaStevens avatar Oct 03 '22 08:10 RebeccaStevens

I had a second look now and my understanding is that prefer-immutable-types will now enforce all readonly types that typescript offers, in all locations in the code, except for type declarations which is instead handled by type-declaration-immutableness. Would that be correct?

jonaskello avatar Oct 04 '22 19:10 jonaskello

Yeah, that's right (interfaces are also covered by type-declaration-immutableness)

RebeccaStevens avatar Oct 05 '22 02:10 RebeccaStevens

@jonaskello We already to release this?

RebeccaStevens avatar Oct 09 '22 12:10 RebeccaStevens

Since the main rule we rely on is being deprecated in this release I feel I need to verify a bit more before I feel comfortable with this release. It's just time constraints that is keeping me from doing it. I'll try to find some time in the very near future, like hopefully later today :-)

One question I can think of right now is about the having prefer-immutable-types and type-declaration-immutableness as separate rules. As far as I understand they both verify that you have used the available compile time readonly declarations that typescript supports, although in different places. Since prefer-immutable-types has options for specifying where to check for readonly (parameters, returnTypes, variables), it seems like type-declaration-immutableness could just be more options on prefer-immutable-types at this point (types, interfaces). Perhaps there is a reason to keep them separate that I am not seeing?

jonaskello avatar Oct 09 '22 12:10 jonaskello

They probably could be merged into one, but I think it would be best to keep them separate. The main reason for this is that prefer-immutable-types is about flagging inline mutable types (when used for with variables or functions). While type-declaration-immutability is about flagging inconsistencies in type alias and interfaces.

type-declaration-immutability doesn't necessarily enforce aliases to be immutable, but rather will apply rules that enforce a min and/or max immutability. With the default "strict" options, it will simply enforce that any alias not prefixed with "Mutable" will have to be immutable.

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

I've started some testing. Took me a while to find why no-this-expression was not working anymore. I guess it is because of this:

rename rules to make them plural. rename a bunch of options too.

Perhaps the change notes could be more explicit of which rules changed :-)

jonaskello avatar Oct 09 '22 14:10 jonaskello

Yeah, I was planning on doing manual write of the release notes for 5.0 rather than using the automated ones. I was just going to use the automated ones for the betas.

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

When using this config:

  rules: {
    "functional/prefer-immutable-types": ["error", { variables: { allowInFunctions: true } }],
  },

I get the error:

Oops! Something went wrong! :(

ESLint: 8.8.0

Error: .eslintrc.js:
        Configuration for rule "functional/prefer-immutable-types" is invalid:
        Value {"allowInFunctions":true} should NOT have additional properties.
        Value {"allowInFunctions":true} should be string,number,boolean.
        Value {"allowInFunctions":true} should be equal to one of the allowed values.
        Value {"allowInFunctions":true} should match exactly one schema in oneOf.

I'm trying to replace the config below for the old rule with the new one, what would be the correct config?

  rules: {
    "functional/prefer-readonly-type": ["error", { allowLocalMutation: true, ignorePattern: "^[mM]utable" }],
  },

jonaskello avatar Oct 09 '22 14:10 jonaskello

The docs for prefer-immutable-types mentions here that the default is allowLocalMutation: false, but the docs above does not list allowLocalMutation as an available option. I guess this is just a leftover from the old rule? The correct config should be allowLocalMutation: true under the variables section instead?

jonaskello avatar Oct 09 '22 14:10 jonaskello

It's supposed to be ignoreInFunctions

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

The docs have both, allowInFunctions needs to be changed in the docs.

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

allowLocalMutation is left over and should be removed.

I'll make these fixes now.

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

Ah, that makes sense. Everything else was named ignoreXXX so I was going to suggest that allowInFunctions should be renamed to ignoreInFunctions but then it is already so :-)

jonaskello avatar Oct 09 '22 14:10 jonaskello

It seems the quick-fix for prefer-immutable-types is not working as in perfer-readonly-type. We use this quick-fix a lot to save typing the readonly stuff by hand.

image

image

jonaskello avatar Oct 09 '22 14:10 jonaskello

The fixer has been removed for now. It's something I want to being back, but I haven't prioritized it.

The reason why it has been removed is because the new fixer will need to work a bit different. For example, with the code you provided above, how should the type be made immutable? It will depend on the enforcement level that is configured. With, ReadonlyShallow or ReadonlyDeep it can simply be changed to readonly string[] but for Immutable, there is no nicely built in way to make an array immutable. Once (/if) https://github.com/sindresorhus/type-fest/pull/479 is merged, ReadonlyDeep<string[]> would work. But as that type comes from a library, does the fixer need to make sure that type is imported? To support other type libraries, we'd also need to make this fix configurable.

This is all do-able but I've put in the do later camp for now.

RebeccaStevens avatar Oct 09 '22 14:10 RebeccaStevens

Given this code:

interface Foo {
  readonly bar: string;
}

function testtwo(quantity: string): ReadonlyArray<Foo> {
  return [{ bar: "" }];
}

and this config:

module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: "module",
    project: "./tsconfig.json",
  },
  plugins: ["functional", "@typescript-eslint", "import"],
  rules: {
    "functional/prefer-immutable-types": ["error", { variables: { ignoreInFunctions: true } }],
  },
};

I get this error:

$ eslint ./src/**/*.ts{,x} --ext .js,.ts,.tsx -f visualstudio
xxxx.ts(9,35): error functional/prefer-immutable-types : Return type should have an immutability of at least "Immutable" (actual: "ReadonlyDeep").

If I change it to return just Foo instead of ReadonlyArray<Foo> it works. I would expect both to work?

jonaskello avatar Oct 09 '22 14:10 jonaskello

ReadonlyArray<T> is not immutable as the methods on the array can be changed. You'll want to lower the enforcement to ReadonlyDeep.

RebeccaStevens avatar Oct 09 '22 15:10 RebeccaStevens

These are the definitions of the different enforcement levels: https://github.com/RebeccaStevens/is-immutable-type#definitions

Immutable: Everything is deeply read only and nothing can be modified. ReadonlyDeep: The data is deeply immutable but methods are not. ReadonlyShallow: The data is shallowly immutable, but at least one deep value is not. Mutable: The data is shallowly mutable.

RebeccaStevens avatar Oct 09 '22 15:10 RebeccaStevens

Would setting ReadonlyDeep on global level give the exact same enforcement as prefer-readonly-types in all places? Currently I'm getting "6559 problems" when switching to prefer-immutable-types in the code base I'm currently working on. So I'm looking for options that would let me migrate without having to change everything :-).

EDIT: With ReadonlyDeep I'm down to 5244 problems so it seemed to have helped at least in part.

jonaskello avatar Oct 09 '22 15:10 jonaskello

Regarding the quick-fix, for us it would be enough that it fixes the same cases as prefer-readonly-types did. Perhaps we can detect if the current config allows for a simple fix and then allow for quick-fix?

jonaskello avatar Oct 09 '22 15:10 jonaskello

I'll probably work on the fixer in stages, the first stage will be adding those simple fixes.

RebeccaStevens avatar Oct 09 '22 15:10 RebeccaStevens

This code:

export type Program<TReturn> = Generator<unknown, TReturn, unknown>;

function partialCall4<TProgramArray extends Array<Program<unknown>>>(
  ...args: [...TProgramArray]
): ExtractProgramArrayReturnTypeTuple<TProgramArray> {
  return args as any;
}

type ExtractProgramArrayReturnTypeTuple<T extends ReadonlyArray<Program<any>>> = {
  [K in keyof T]: T[K] extends Program<infer V> ? V : never;
};

Gives the errors:

$ eslint ./src/**/*.ts{,x} --ext .js,.ts,.tsx -f visualstudio

Oops! Something went wrong! :(

ESLint: 8.25.0

TypeError: Cannot read properties of undefined (reading 'flags')
Occurred while linting /home/jonkel/code/experiment/estest/src/a.ts:3
Rule: "functional/prefer-immutable-types"
    at Object.isUnionType (/home/jonkel/code/experiment/estest/node_modules/tsutils/typeguard/2.8/type.js:70:18)
    at unionTypeParts (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:116:19)
    at isPropertyReadonlyInType (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:200:21)
    at isReadonlyPropertyFromMappedType (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:252:12)
    at /home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:229:21
    at someTypePart (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:125:52)
    at isReadonlyPropertyIntersection (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:222:12)
    at Object.isPropertyReadonlyInType (/home/jonkel/code/experiment/estest/node_modules/tsutils/util/type.js:211:43)
    at objectImmutability (/home/jonkel/code/experiment/estest/node_modules/is-immutable-type/dist/index.cjs:290:25)
    at calculateTypeImmutability (/home/jonkel/code/experiment/estest/node_modules/is-immutable-type/dist/index.cjs:264:16)

The settings:

module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: "module",
    project: "./tsconfig.json",
  },
  plugins: ["functional", "@typescript-eslint", "import"],
  rules: {
    // "functional/prefer-readonly-type": ["error", { allowLocalMutation: true, ignorePattern: "^[mM]utable" }],
    "functional/prefer-immutable-types": [
      "error",
      { enforcement: "ReadonlyDeep", variables: { ignoreInFunctions: true } },
    ],
  },
};

jonaskello avatar Oct 10 '22 05:10 jonaskello

Looks like a bug with tsutils

RebeccaStevens avatar Oct 11 '22 03:10 RebeccaStevens

https://github.com/ajafff/tsutils/issues/147

RebeccaStevens avatar Oct 11 '22 03:10 RebeccaStevens

@jonaskello I've update some of the logic and dependencies, do you want to retest and see if you are still having that issue?

RebeccaStevens avatar Oct 17 '22 04:10 RebeccaStevens