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

Rule proposal: [prefer-at] Rule to prefer array.at(-1) over array[array.length - 1] #6401

Open
6 tasks done
JoshuaKGoldberg opened this issue Jan 31, 2023 · 14 comments
Open
6 tasks done
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin

Comments

@JoshuaKGoldberg
Copy link
Member

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
  • My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
  • I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

#6211 shows a use of array.at(-1), as a cleaner equivalent to array[array.length - 1]. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at

Proposal: let's add a typed lint rule that, when it sees something like array[array.length - 1] on an Array-typed value, proposes fixing to array.at(-1)?

Fail Cases

declare const array: string[];

array[array.length - 1];

Pass Cases

declare const array: string[];

array.at(-1);

// [] indices are reasonable
array[0];

// Don't look through functions
const getArray = () => array;
getArray()[getArray().length - 1];

Additional Info

Reference for an existing similar rule: https://typescript-eslint.io/rules/prefer-includes.

I don't think we should enable this in recommended or strict configs until we only support Node versions that include .at. Per #6396 this wouldn't make sense for users who compile to Node 14 and don't include a polyfill.

@JoshuaKGoldberg JoshuaKGoldberg added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for maintainers to take a look enhancement: new plugin rule New rule request for eslint-plugin labels Jan 31, 2023
@bradzacher bradzacher added accepting prs Go ahead, send a pull request that resolves this issue and removed triage Waiting for maintainers to take a look labels Feb 1, 2023
sviat9440 pushed a commit to MillerSvt/typescript-eslint that referenced this issue Feb 2, 2023
sviat9440 added a commit to MillerSvt/typescript-eslint that referenced this issue Feb 2, 2023
@coyotte508
Copy link
Contributor

// Don't look through functions
const getArray = () => array;
getArray()[getArray().length - 1];

Why ? 🤔 (sorry 😅)

Also, do we want to limit to -1?

@sviat9440
Copy link
Contributor

sviat9440 commented Feb 2, 2023

Why ? (sorry )

I think we can't guarantee that the function always returns the same array. But it seems to be a rarity. In most cases, it can be replaced with .at(-1). Let's see what others have to say.

Also, do we want to limit to -1?

Hmm, indeed, if arr[arr.length - 1] is equivalent to array.at(-1), then why not do the same for other numbers?
@JoshuaKGoldberg , what do you think?

@JoshuaKGoldberg
Copy link
Member Author

Why 🤔 (sorry 😅)

I think we can't guarantee that the function always returns the same array. But it seems to be a rarity. In most cases, it can be replaced with .at(-1). Let's see what others have to say.

Haha, yeah, these very simplified code snippets often look ridiculous. But we have to imagine that sometimes there will be some code somewhere that implements those odd patterns. And if that code is just barely not bizarre enough to be ignored, then we have to consider it as a valid edge case for the rule.

In this case, it's not uncommon for people to come from traditional OOP style programming paradigms and write code like:

declare class Box<T> {
  constructor(private readonly value: T);
  getValue(): T;
  // maybe some other methods, like updateValue(newValue: T)
}

const box = new Box([Math.random(), Math.random()]);
const lastRandomNumber = box.getValue()[box.getValue().length - 1];

...but we as the linter might not know that getValue() always returns the same thing.

Hmm, indeed, if arr[arr.length - 1] is equivalent to array.at(-1), then why not do the same for other numbers? @JoshuaKGoldberg , what do you think?

Yeah makes sense to me! Let's say any constant literal or number after the -, such as 1, 2, i, etc.

@sviat9440
Copy link
Contributor

but we as the linter might not know that getValue() always returns the same thing.

How about creating an option: ignore functions or not?

@Josh-Cena
Copy link
Member

ignore functions

I don't think so. It's really hard to generalize whether a codebase has all side-effect-free functions or not, and configuring such an option could easily break code in the future. We should just assume all functions are side-effect-ful, because I don't feel like sane code that's meant to be side-effect-free would be written that way anyway.

@sviat9440
Copy link
Contributor

sviat9440 commented Feb 2, 2023

that's meant to be side-effect-free

We can just display a warning for functions without autofix.

@Josh-Cena
Copy link
Member

Sounds good to me. We can autofix "definitely safe" things (which honestly only includes identifiers, but we can assume member expressions are generally safe as well), while keeping others as suggestions.

@bradzacher
Copy link
Member

Semantically I think that this rule should be safe to fix through a function call.

There really no reasonable code which should look like fn()[fn().length - 1].
Thinking through it - the only time that wouldn't be safe to fix is if the function returns some predictable pattern of arrays. We're talking about an edge of an edge case here, IMO.

If you wanted to split hairs about safety:

const x = {
  get arr() {
    return Array(Math.random());
  },
};

x.arr[x.arr.length - 1];

Technically it's not safe to fix member expressions due to getters or to proxies!

I think we can safely assume that if you call the same function in the indexer and the expression then you're working with the same value.

Definitely agree we can add an option to disable that behaviour - but I would expect it to be an opt-out, I.e. Default on, because it's likely the most common case.

@Josh-Cena
Copy link
Member

If you wanted to split hairs about safety

That's exactly what I said with "we can assume member expressions are generally safe" :) But IMO we have to make trade-offs here about what's likely and what's not.

@omril1
Copy link
Contributor

omril1 commented Feb 7, 2023

  1. I think the rule should apply to strings as well
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at
  2. There is a similar rule in https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-at.md

@sviat9440
Copy link
Contributor

How about this case?

      declare let a: string;

      const lastItem = (a += 'test')[(a += 'test').length - 1];

Should it be converted to a: ?

      declare let a: string;

      const lastItem = (a += 'test').at(-1);

Now - it's being transformed

@bradzacher
Copy link
Member

@sviat9440 Is that a hypothetical case, or code you've legitimately encountered in the wild?

@sviat9440
Copy link
Contributor

sviat9440 commented Jun 29, 2023

It is hypothetical case. It seems that transformation in this case can lead to unexpected results. Especially if we subtract -i instead of -1.

@bradzacher
Copy link
Member

This code is also correct code that is unsafe to fix:

let x = 0;
const arr = [1,2,3];

const lastItem1 = (x += 1, arr)[(x += 1, arr).length - 1];

// or

declare let cond: boolean;
const lastItem2 = (cond ? (cond = !cond, []) : (cond = !cond, arr))[
	(cond ? (cond = !cond, []) : (cond = !cond, arr)).length - 1
];

My point is that we could sit here all day dreaming up "semantically correct code a user may write that we shouldn't fix"! The joy of an expression location is that there is literally an infinite number of permutations of code that has side-effects and is semantically correct in the location!

My take on this is that we're talking about code that people won't ever write in the real world - so it's not worth the maintenance burden and complexity of handling and testing for them.

If someone comes to us with a real world example of their code that they have to write in such an insane style - then, and only then we could consider handling the case (though TBH I'd probably tell them to just use a disable comment...). Otherwise I think we should just "assume good intent" and assume that in the case of T[T - i] - T is an expression without side-effects which I would guess is an assumption that covers 99.99999999% of real code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepting prs Go ahead, send a pull request that resolves this issue enhancement: new plugin rule New rule request for eslint-plugin package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin
Projects
None yet
Development

No branches or pull requests

6 participants