Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add strict-type-predicates #738

Conversation

bradleyayers
Copy link
Contributor

This is a direct port of the TSLint version, with as few changes as possible.

Notable differences:

  • No warning emitted when strictNullChecks is not enabled.
  • Shorter rule description
  • Only a single set of tests (instead of split unknown, strictNullChecks, noStrictNullChecks)

Copy link
Member

@bradzacher bradzacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't read the code yet, but I noticed a few things:

  • you forgot a rule readme
  • there needs to be an entry in the root eslint-plugin readme
  • there should be an entry in configs/all.json

run:

  • yarn generate:configs to add it to all.json
  • yarn check:docs to validate your docs setup

@bradzacher bradzacher added enhancement: new plugin rule New rule request for eslint-plugin awaiting response Issues waiting for a reply from the OP or another party labels Aug 7, 2019
@bradzacher bradzacher removed the awaiting response Issues waiting for a reply from the OP or another party label Sep 17, 2019
@bradleyayers
Copy link
Contributor Author

bradleyayers commented Sep 25, 2019

Hey @bradzacher any more thoughts on this? I'm thinking that I'll use a local fork for my own project to give it some real-world testing. Are there any guides on how to do this (or is it as simple as npm pack)?

@bradzacher
Copy link
Member

bradzacher commented Sep 26, 2019

It's on my todo list. I've had family visiting for a few weeks so my spare time has been sparse.

I'm about to start getting back into the review queue.

I've never done a local fork, sorry, so I don't have any advice for you. I'd assume it should be pretty simple because the publish step is literally just publish each package's directory.

@ark120202
Copy link

Looks like it has some intersection with no-unnecessary-condition, at least in that case:

const number = 1;
if (number === null) {}
if (number === undefined) {}

It doesn't handle that though:

const numberOrNull: number | null = 0;
if (numberOrNull != null) {}

const number: number = 0;
if (typeof number === 'string') {}

I wonder if there's really a need for multiple rules being so similar (conceptually)?
Would it maybe be better to improve no-unnecessary-condition instead?

@bradleyayers
Copy link
Contributor Author

I like the idea of merging it into no-unnecessary-condition, that seems like it could be a good fit. I think the name is an improvement too. I feel like it might need some options to enable the new behaviour though, as it's quite strict and might be undesirable for some people.

@bradzacher
Copy link
Member

I'm curious - how much value is there for you in these checks?

const numberOrNull: number | null = 0;
// Implicitly checks for `!== undefined`, which is always false.
if (numberOrNull != null) {
  return;
}
const numberOrUndefined: number | undefined = 0;
// Implicitly checks for `!== null`, which is always false.
if (numberOrNull != undefined) {
  return;
}

I've always held the opinion that null checks should always be made [!=]= null, because it catches the undefined case - treating null and undefined differently seems like a path to bugs IMO.


This does seem like it has value for obvious reasons:

const number: number = 0;
// Always false.
if (typeof number === 'string') {
  return;
}

@bradleyayers
Copy link
Contributor Author

In general I've found the rule really valuable for killing stale code, increasing confidence in refactors, and readability (you know exactly what types are being dealt with). The trade-off of having more verbosity in exchange for more clarity has been one we've been happy with.

Specifically regarding null/undefined comparisons, they're good for refactors where a null might be swapped for an undefined, but a !== null is left unchanged in the code. Like you say though, another option is to ban !== for null/undefined, and force null/undefined comparisons to use !=. I think that's a reasonable direction to go, and encourage consistency with optional chaining.

What rule do you use to enforce that?

@bradzacher
Copy link
Member

What rule do you use to enforce that?

The base eslint eqeqeq rule will cover it, if you pass the second argument:
["error", "always", {"null": "never"}] - eslint demo


I'd be happy to have both in no-unnecessary-condition:

  • the typeof check can probably be straight up built into the rule
  • == null can be added as an option to the rule.
    • I think that it is ultimately a stylistic choice as to whether you use x == null or x === null || x === undefined, so I think having an option to be super strict and trust types completely is 100% okay!

@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label Nov 12, 2019
@bradzacher bradzacher added the help wanted Extra attention is needed label Dec 19, 2019
@armano2 armano2 removed the help wanted Extra attention is needed label Jan 8, 2020
@bradzacher bradzacher removed the awaiting response Issues waiting for a reply from the OP or another party label Jan 8, 2020
Copy link
Member

@bradzacher bradzacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for being patient.
a few comments about the place

mostly looks good.
thanks for doing this.

I think it'd be great if the (==|!=|===|!===) (null|undefined) checks had a flag to disable it.
It might just be me being selfish, because I like everything but that check :)

Comment on lines +47 to +51
const number: number = 0;
// Always false.
if (typeof number === 'number') {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be reported by the rule, because it's always true?

This should probably follow the above tests

const numberOrUndefined: number | undefined = 0;

Comment on lines +37 to +51
const numberOrNull: number | null = 0;
if (numberOrNull !== null) {
return;
}

const numberOrUndefined: number | undefined = 0;
if (numberOrNull !== undefined) {
return;
}

const number: number = 0;
// Always false.
if (typeof number === 'number') {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit with these - just for correctness, typescript narrows the type of the const variables to just be number, because it knows they can't change, so they can't be undefined.

A good way around this is to either declare the variables:

declare const numberOrUndefined: number | undefined;
if (numberOrNull !== undefined) {
  return;
}

Or wrap your examples in a function:

function ex1(numberOrUndefined: number | undefined) {
  if (numberOrNull !== undefined) {
    return;
  }
}

Comment on lines 12 to 16
type Options = [
{
typesToIgnore?: string[];
},
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you removed this option from the rule

type Options = [];

},
schema: [],
},
defaultOptions: [{}],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
defaultOptions: [{}],
defaultOptions: [],

Comment on lines 197 to 208
return isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks')
? {
BinaryExpression(node: TSESTree.BinaryExpression): void {
const equals = getEqualsKind(node.operator);
if (equals !== undefined) {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
checkEquals(tsNode as ts.BinaryExpression, node, equals);
}
},
}
: // TODO: emit warning that strictNullChecks is required
{};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to disable the whole rule if strictNullChecks isn't enabled?
We could probably just disable the null/undefined checks instead.

const equals = getEqualsKind(node.operator);
if (equals !== undefined) {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
checkEquals(tsNode as ts.BinaryExpression, node, equals);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be done automatically now

Suggested change
checkEquals(tsNode as ts.BinaryExpression, node, equals);
checkEquals(tsNode, node, equals);

Comment on lines +328 to +336
return !anyOther
? true
: anyNull && anyUndefined
? undefined
: anyNull
? 'null'
: anyUndefined
? 'undefined'
: false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - in cases like this I find that a series of if statements are a lot easier to read

Comment on lines +47 to +50
// get<number | null>() === null;
// get<number | undefined>() === undefined;
// get<string | null | undefined>() == null;
// get<string | null | undefined>() != undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uncomment or delete

Comment on lines +174 to +186
{
code: `
declare function get<T>(): T;
enum E {}
typeof get<E>() === "number";`,
errors: [
{
messageId: 'expressionAlwaysTrue',
line: 4,
column: 1,
},
],
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for completeness, would be good to get a string enum test enum E { a = 'aaa' }.

],
},

// typeof symbol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just cos you've got a theme going, would be good to add a test for Symbol.

@bradzacher bradzacher added the awaiting response Issues waiting for a reply from the OP or another party label Jan 16, 2020
@codecov
Copy link

codecov bot commented Jan 18, 2020

Codecov Report

Merging #738 into master will decrease coverage by 0.02%.
The diff coverage is 93.91%.

@@            Coverage Diff             @@
##           master     #738      +/-   ##
==========================================
- Coverage   95.55%   95.53%   -0.03%     
==========================================
  Files         145      146       +1     
  Lines        6576     6691     +115     
  Branches     1878     1917      +39     
==========================================
+ Hits         6284     6392     +108     
- Misses        111      112       +1     
- Partials      181      187       +6     
Impacted Files Coverage Δ
.../eslint-plugin/src/rules/strict-type-predicates.ts 93.85% <93.85%> (ø)
packages/eslint-plugin/src/rules/index.ts 100.00% <100.00%> (ø)

@felixfbecker
Copy link

Would really love to see this, one of the last rules that still requires me to use TSLint!

@ark120202
Copy link

So how would this rule relate to no-unnecessary-condition, especially with #1659?

@bradzacher
Copy link
Member

bradzacher commented Mar 3, 2020

#1659 will deal with the null/undefined case (which I was asking to be behind an option in here anyway)

But no-unnecessary-condition does not currently check the typeof operator.
No reason it couldn't be augmented with it. Just depends on how much functionality we want to load up into that one rule.

@bradzacher bradzacher added the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Jun 24, 2020
@bradzacher bradzacher closed this Jun 24, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
awaiting response Issues waiting for a reply from the OP or another party enhancement: new plugin rule New rule request for eslint-plugin stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants