GithubHelp home page GithubHelp logo

danielnixon / eslint-plugin-total-functions Goto Github PK

View Code? Open in Web Editor NEW
87.0 4.0 4.0 2.85 MB

An ESLint plugin to enforce the use of total functions (and prevent the use of partial functions) in TypeScript.

License: MIT License

JavaScript 1.06% TypeScript 98.94%
typescript eslint eslint-plugin functional-programming type-safety partial-functions

eslint-plugin-total-functions's Introduction

TypeScript Total Functions - ESLint Plugin

Build Status Type Coverage Test Coverage Mutation testing badge Known Vulnerabilities Codacy Badge npm

An ESLint plugin to enforce the use of total functions (and prevent the use of partial functions) in TypeScript. If you like your types to tell the truth, this is the ESLint plugin for you.

Version Matrix

TypeScript ESLint eslint-plugin-total-functions Suppported?
5.0.2 8.36.0 7.0.0
4.9.5 8.35.0 6.2.0 No
4.7.3 8.17.0 6.0.0 No
4.5.4 8.5.0 5.0.0 No
4.4.2 7.32.0 4.10.1 No
4.3.5 7.30.0 4.8.0 No
4.1.2 7.12.0 4.7.2 No
4.0.2 7.9.0 3.3.0 No

Installation

yarn add --dev eslint-plugin-total-functions \
  @typescript-eslint/parser \
  eslint \
  typescript

Setup

Option 1

Use eslint-config-typed-fp which includes this plugin among others.

Option 2

  1. Turn on TypeScript's strict mode and noUncheckedIndexedAccess option.
  2. Set up ESLint + TypeScript.
  3. Turn on eslint-plugin-functional (recommended). Its rules related to mutation and OO are more important than this plugin's rules and they'll help keep your types honest.
  4. Update your .eslintrc.js:
module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    project: "./tsconfig.json",
    ecmaVersion: 2018,
    sourceType: "module"
  },
  extends: [
+    "plugin:total-functions/recommended",
  ],
  plugins: [
+    "total-functions",
  ],
};

Alternatively you can configure individual rules separately (see below).

Rules

Rule Recommended All Fixer?
require-strict-mode
no-unsafe-type-assertion
no-unsafe-readonly-mutable-assignment
no-unsafe-mutable-readonly-assignment
no-enums
no-partial-url-constructor
no-partial-division
no-partial-string-normalize
no-premature-fp-ts-effects
no-nested-fp-ts-effects
no-partial-array-reduce
no-hidden-type-assertions

Deprecated rules

total-functions/require-strict-mode

The world is a very strange place when strict mode is disabled. This rule enforces strict mode and noUncheckedIndexedAccess mode (which is sadly not included under the strict umbrella).

total-functions/no-unsafe-type-assertion

Bans unsafe type assertions, for example:

type Foo = { readonly bar: number };
const foo = {} as Foo; // This compiles
foo.bar.toString(); // This explodes at runtime

This is similar to the consistent-type-assertions rule from typescript-eslint, however:

  1. this rule is weaker than consistent-type-assertions with its assertionStyle option set to never -- this rule will permit type assertions that it considers safe as opposed to blanket banning all type assertions, and
  2. this rule is stronger than consistent-type-assertions with its objectLiteralTypeAssertions option set to never, for example:
type Foo = { readonly bar: number };
const foo = {};
const foo2 = foo as Foo; // Flagged by this rule, but not by consistent-type-assertions (unless you set assertionStyle to never)
foo2.bar.toString(); // This explodes at runtime

For examples of type assertions that this rule considers valid and invalid, see no-unsafe-type-assertion.test.ts.

See TypeScript issue #7481 for a request to fix this at the language level.

total-functions/no-unsafe-readonly-mutable-assignment

Bans unsafe assignment of readonly values to mutable values (which can lead to surprising mutation in the readonly value). This includes passing readonly values as arguments to functions that expect mutable parameters.

For examples of assignment that this rule considers valid and invalid, see no-unsafe-readonly-mutable-assignment.test.ts.

See TypeScript issue #13347 for a request to fix this at the language level.

total-functions/no-unsafe-mutable-readonly-assignment

The inverse counterpart to no-unsafe-readonly-mutable-assignment. This rule bans unsafe assignment of mutable values to readonly values (which just like the inverse can lead to surprising mutation in the readonly value).

This rule is often noisy in practice so, unlike no-unsafe-readonly-mutable-assignment, is excluded from the recommended config.

Note that the following is considered an assignment from mutable to readonly:

  type ReadonlyA = { readonly a: string };
  const readonlyA: ReadonlyA = { a: "" };

The solution is to append as const to the RHS:

  type ReadonlyA = { readonly a: string };
  const readonlyA: ReadonlyA = { a: "" } as const;

For examples of assignment that this rule considers valid and invalid, see no-unsafe-mutable-readonly-assignment.test.ts.

total-functions/no-enums

Enums have a number of issues, including unsoundness issues (which are especially relevant here). This rule bans the declaration of enums entirely. Use an alternative such as a union of strings instead.

total-functions/no-unsafe-enum-assignment

Deprecated No longer required as of TypeScript 5 (https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#all-enums-are-union-enums).

If you do use an enum (or are forced to by a library), this rule flags unsafe assignment that the TypeScript compiler (prior to version 5) permits. For example:

  enum ZeroOrOne {
    Zero = 0,
    One = 1,
  }

  // This compiles (prior to TypeScript 5) but is flagged by no-unsafe-enum-assignment
  const zeroOrOne: ZeroOrOne = 2;

  // This is not flagged by no-unsafe-enum-assignment
  const zeroOrOne: ZeroOrOne = ZeroOrOne.Zero;

total-functions/no-partial-url-constructor

The URL constructor can throw (i.e. it is partial).

// This compiles and foo appears to be a URL. It isn't.
const foo: URL = new URL(""); // Throws TypeError [ERR_INVALID_URL]: Invalid URL

Instead, you should use a wrapper that catches that error and returns URL | undefined or similar (perhaps using an Option type).

URL also happens to be mutable, which will be flagged by prefer-immutable-types. The readonly-types package provides a readonlyURL function that solves both of these issues.

total-functions/no-partial-division

Division by zero is undefined. That makes the division operator partial.

In the case of number, it results in Infinity (IEEE 754...).

In the case of bigint it throws a RangeError.

The latter is much more indisputably partial than the former.

> 1 / 0
Infinity
> 1n / 0n
Uncaught RangeError: Division by zero

This rule flags division unless the denominator is provably non-zero. If you need division, you should wrap it in a wrapper that returns undefined when the denominator is zero. Alternatively, consider using branded types / refinements, such as https://github.com/gcanti/io-ts/blob/master/index.md#branded-types--refinements or https://gcanti.github.io/newtype-ts/modules/NonZero.ts.html

See Also

eslint-plugin-total-functions's People

Contributors

danielnixon avatar dependabot-preview[bot] avatar dependabot[bot] avatar haolinj avatar renovate-bot avatar renovate[bot] avatar willheslam avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

eslint-plugin-total-functions's Issues

Allow type widening in no-unsafe-type-assertion

Type assertions can be used to widen types. The no-unsafe-type-assertion rule currently flags those usages. It probably shouldn't.

E.g. these should pass the rule:

const foo = "foo" as string;
const bar = 1 as number;

Object indexing false positive

I'm wondering why the following example errors:

const periods = {
    oneDay: () => subDays(Date.now(), 1).valueOf(),
    oneWeek: () => subWeeks(Date.now(), 1).valueOf(),
    twoWeeks: () => subWeeks(Date.now(), 2).valueOf(),
    oneMonth: () => subMonths(Date.now(), 1).valueOf(),
    sixMonths: () => subMonths(Date.now(), 6).valueOf()
};
const period: keyof typeof periods = 'oneDay';
periods[period];

Great rule by the way, picking up lots of bugs.

[no-unsafe-subscript] doesn't catch some cases due to definite assignment analysis

Observe this annoying case:

const arr: readonly number[] = [];
const foo: number | undefined = arr[0]; // NOT flagged by no-unsafe-subscript
foo.toString(); // compiles; explodes at runtime

This is caused by definite assignment analysis. Even though foo has an explicit type annotation of number | undefined, TypeScript determines that foo cannot be undefined because it is "definitely assigned". toString() then explodes at runtime.

Our no-unsafe-subscript rule currently doesn't flag const foo: number | undefined = arr[0] because the contextual type of the expression arr[0] is number | undefined (the type of the value it's being assigned to).

This demonstrates that we can't assume the array access is safe even when the contextual type contains undefined -- at least for assignment expressions, which as far as I can tell are the only place that definite assignment analysis applies.

We will have to update no-unsafe-subscript to either ignore the contextual type in all cases (which would cause lots of pain and what amount to false positives) or only in assignment expressions (if possible).

🤦

`as` can lead to surprise mutation

Even with both the rules no-unsafe-assignment and no-unsafe-type-assertion enabled, this isn't complained about:

type MutableA = { a: string };
type ReadonlyA = { readonly a: string };

const ro: ReadonlyA = { a: "" };
const mut = ro as MutableA; // This shouldn't be allowed

It's not immediately clear which of those two rules should catch this, but at least one of them should.

Support noUncheckedIndexedAccess

This is a tracking ticket for microsoft/TypeScript#39560.

When that lands, we will need to:

  1. Probably bail out of the no-unsafe-subscript rule early if that compiler flag is on (unless there are any remaining problematic cases). Or deprecate/remove this rule entirely?
  2. Ditto for no-unsafe-destructuring.
  3. Update the require-strict-mode rule to also check for this flag and complain if it isn't turned on. This assumes we'll have to enable noUncheckedIndexedAccess separately from strict, otherwise this point is just subsumed by #73.

TypeScript 4 support

Need to update no-unsafe-assignment to cover the new short-circuiting assignment operators.

TSX test cases

... and any relaxation of the rules to allow safe usages required in response

New rule: unsafe assignment / declaration / return / call

Prevent this:

type A = { val: string; };
type B = { val: string | number; };

const a: A = { val: "string" };
const b: B = a;

b.val = 3;

console.log(a.val.toUpperCase()); // throws

And this:

interface MutableValue<T> {
    value: T;
}

interface ImmutableValue<T> {
    readonly value: T;
}

let i: ImmutableValue<string> = { value: "hi" };
i.value = "Excellent, I can't change it"; // compile-time error

let m: MutableValue<string> = i;
m.value = "Oh dear, I can change it";

see microsoft/TypeScript#14150 and microsoft/TypeScript#13347

total-functions/no-array-destructuring false positive with string literals

The code that reproduces the error:

interface Response {
  'error-code': number
}

function getError(
  { 'error-code': errorCode }: Response // <-- total-functions/no-array-destructuring
): string {
  return errorCode.toString()
}

There should be no total-functions/no-array-destructuring in this code.
For normal properties (not quoted), everything works fine.

I am using the library version:

Eslint config:

extends: [
  ...,
  'plugin:total-functions/recommended',
],
plugins: [
  ...,
  'total-functions'
]

Without rewriting the rules for eslint-plugin-total-functions.

Optional Array Access - Should be Valid

The primary way I would like to access an array value is with the ?. (optional chain operator) to access a value from an array:

        const arr: ReadonlyArray<number> = [];
        const foo = arr?.[0]; 
        // Type is number | undefined;

This is essentially the same as:

        const foo = arr[0] as number | undefined;

So this should be a valid pattern, not an invalid pattern.

From: https://github.com/danielnixon/eslint-plugin-total-functions/blob/master/src/rules/no-array-subscript.test.ts

In the invalid patterns:

// optional member array access
    {
      filename: "file.ts",
      code: `
        const arr: ReadonlyArray<number> = [];
        const foo = arr?.[0];
      `,
      errors: [
        {
          messageId: "errorStringGeneric",
          type: AST_NODE_TYPES.OptionalMemberExpression,
        },
      ],
    },

Am I missing something?

New rule: ban unsafe type assertions

These shouldn't be flagged:

const foo = "foo" as unknown; // safe
const foo = "foo" as const; // safe
const arr = ["foo"];
const foo = arr[0] as string | undefined; // safe

But things like this should be:

type Foo = {
  readonly bar: number;
  readonly bas: string;
};

const foo = {} as Foo;

foo.bar.toString(); // Explodes at runtime

Cannot read property 'isUnion' of undefined

I receive an error from the no-array-destructuring rule at this line:
const { items: [{ phone }] } = await response.json();

The stack trace is:

[Error - 1:43:28 AM] TypeError: Cannot read property 'isUnion' of undefined
    at ArrayPattern (node_modules/eslint-plugin-total-functions/dist/rules/no-array-destructuring.js:44:51)
    at node_modules/eslint/lib/linter/safe-emitter.js:45:58
    at Array.forEach (<anonymous>)
    at Object.emit (node_modules/eslint/lib/linter/safe-emitter.js:45:38)
    at NodeEventGenerator.applySelector (node_modules/eslint/lib/linter/node-event-generator.js:254:26)
    at NodeEventGenerator.applySelectors (node_modules/eslint/lib/linter/node-event-generator.js:283:22)
    at NodeEventGenerator.enterNode (node_modules/eslint/lib/linter/node-event-generator.js:297:14)
    at CodePathAnalyzer.enterNode (node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js:635:23)
    at node_modules/eslint/lib/linter/linter.js:949:32
    at Array.forEach (<anonymous>)```

false positive 'total-functions/no-unsafe-type-assertion' rule is disabled but never reported eslint-comments/no-unused-disable

public getMessages(locale?: Locale): IntlMessages {
    if (locale !== undefined) {
      const messages = this.messages.get(locale)

      if (messages !== undefined) {
        return messages
      }
    }

    const messages = this.messages.get(this.locale)

    if (messages !== undefined) {
      return messages
    }

    // eslint-disable-next-line total-functions/no-unsafe-type-assertion
    return {} as IntlMessages
  }

Error "'total-functions/no-unsafe-type-assertion' rule is disabled but never reported" doesn't show up in vscode, but shows up in terminal

Basically I need to return empty object at the end, so it has same type.

Rename rules

no-array-subscript => no-unsafe-subscript
no-array-destructuring => no-unsafe-destructuring

(because they also cover objects and now only ban unsafe cases)

New rule: Optional properties lead to unsoundness

This is unsettling:

type Foo = { readonly foo: string };
type Bar = { readonly foo: string, readonly bar?: () => unknown };
// This won't compile
// const foo: Foo = { foo: "foo", bar: "bar" };
// But this will
const thing = { foo: "foo", bar: "bar" };
const foo: Foo = thing;
// Uh oh...
const bar: Bar = foo;
if (bar.bar !== undefined) {
    // Runtime explosion
    // [ERR]: Executed JavaScript Failed: 
    // [ERR]: bar.bar is not a function 
    bar.bar();
}

It’s hard to point at precisely which line is “wrong”.

We may want a new rule that forbids:

const foo: Foo = thing;

We can think of it as taking the tsc error reported by

const foo: Foo = { foo: "foo", bar: "bar" };

and expanding its scope beyond just object literals.

Allow array subscript access if return value is immediately assigned to a type that includes undefined

This would allow something like this without complaining:

const last: <A>(array: ReadonlyArray<A>) => A | undefined = (a) => a[a.length - 1];

These are cases where the partiality isn't really observable, similar to the ignoreImmediateMutation option in https://github.com/jonaskello/eslint-plugin-functional/blob/master/docs/rules/immutable-data.md#options

We probably want an option to disable this too.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.