Skip to content

Commit

Permalink
Merge pull request #104 from ShridharGoel/prefer-at
Browse files Browse the repository at this point in the history
Use at() instead of direct access
  • Loading branch information
roryabraham authored Jul 31, 2024
2 parents 93110ff + ec81066 commit 88309c5
Show file tree
Hide file tree
Showing 9 changed files with 2,374 additions and 3,446 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
.DS_Store

.idea
*.iml
1 change: 1 addition & 0 deletions eslint-plugin-expensify/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ module.exports = {
NO_ACC_SPREAD_IN_REDUCE: 'Avoid a use of spread (`...`) operator on accumulators in reduce callback. Mutate them directly instead.',
PREFER_TYPE_FEST_TUPLE_TO_UNION: 'Prefer using `TupleToUnion` from `type-fest` for converting tuple types to union types.',
PREFER_TYPE_FEST_VALUE_OF: 'Prefer using `ValueOf` from `type-fest` to extract the type of the properties of an object.',
PREFER_AT: 'Prefer using the `.at()` method for array element access.',
},
};
107 changes: 107 additions & 0 deletions eslint-plugin-expensify/prefer-at.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const { AST_NODE_TYPES, ESLintUtils } = require('@typescript-eslint/utils');
const message = require('./CONST').MESSAGE.PREFER_AT;

module.exports = {
meta: {
fixable: 'code',
},
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const typeChecker = parserServices.program.getTypeChecker();

function isArrayType(node) {
if (node.type === 'ArrayExpression') {
return true;
}
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const type = typeChecker.getTypeAtLocation(tsNode);

return typeChecker.isArrayType(type);
}

function getSourceCode(node) {
return context.getSourceCode().getText(node);
}

function parseExpression(node) {
switch (node.type) {
case AST_NODE_TYPES.Literal:
if (typeof node.value === 'number') {
return node.value.toString();
}
return null;

case AST_NODE_TYPES.BinaryExpression:
const left = parseExpression(node.left);
const right = parseExpression(node.right);
if (left !== null && right !== null) {
return `(${left} ${node.operator} ${right})`;
}
return null;

case AST_NODE_TYPES.UnaryExpression:
const argument = parseExpression(node.argument);
if (argument !== null) {
return `${node.operator}${argument}`;
}
return null;

case AST_NODE_TYPES.MemberExpression:
if (node.property.type === 'Identifier' && node.property.name === 'length') {
return `${getSourceCode(node.object)}.length`;
}
return null;

case AST_NODE_TYPES.Identifier:
return node.name;

default:
return null;
}
}

function getExpressionWithUpdatedBrackets(indexExpression) {
if (indexExpression.startsWith('(') && indexExpression.endsWith(')')) {
return indexExpression.slice(1, -1);
}
return indexExpression;
}

function checkNode(node) {
if (node.type === AST_NODE_TYPES.MemberExpression && node.property) {
if (!isArrayType(node.object)) {
return;
}

const indexExpression = parseExpression(node.property);

if (indexExpression !== null && indexExpression !== 'length' && indexExpression !== 'at') {
context.report({
node,
message,
fix(fixer) {
const objectText = getSourceCode(node.object);
return fixer.replaceText(node, `${objectText}.at(${getExpressionWithUpdatedBrackets(indexExpression)})`);
},
});
}
}
}

function shouldIgnoreNode(node) {
return (
node.parent &&
node.parent.type === AST_NODE_TYPES.MemberExpression &&
node.parent.property === node
);
}

return {
MemberExpression(node) {
if (!shouldIgnoreNode(node)) {
checkNode(node);
}
},
};
},
};
Empty file.
127 changes: 127 additions & 0 deletions eslint-plugin-expensify/tests/prefer-at.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const RuleTester = require('@typescript-eslint/rule-tester').RuleTester;
const rule = require('../prefer-at');
const message = require('../CONST').MESSAGE.PREFER_AT;

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
ecmaVersion: 2020,
},
});

ruleTester.run('prefer-at', rule, {
valid: [
{
code: 'const example = [1, 2, 3, 4]; example.at(0)',
},
{
code: 'const test = [1, 2, 3, 4]; test.at(1)',
},
{
code: 'const sample = [1, 2, 3, 4]; sample.at(-1)',
},
{
code: 'const testing = [1, 2, 3, 4]; testing.at(-2)',
},
{
code: 'const testing = [[1, 2], [3, 4]]; testing.at(0).at(-1)',
},
{
code: 'const test = [1, 2, 3, 4]; test.at((test.length - 1) / 2)',
},
{
code: `
const object = {0: 'v0', 1: 'v1', 2: 'v2'};
object[0]
`,
},
{
code: `
const index = 1;
const object = {0: 'v0', 1: 'v1', 2: 'v2'};
object[index]
`,
},
{
code: '[0, 1, 2].at(1)',
},
{
code: 'const a = [1, 2, 3, 4]; a.at(a.length - 1)',
},
{
code: 'const a = [1, 2, 3, 4]; a.at(-2)',
},
{
code: 'const a = [1, 2, 3, 4]; const index = 1; a.at(index)',
},
{
code: 'const obj = { a: 1, b: 2 }; obj["a"]',
},
{
code: 'const mixed = [1, { a: 2 }, 3]; mixed.at(1).a',
},
{
code: 'const a = ["a", "b", "c"] as const; a[0]',
},
],
invalid: [
{
code: 'const example = [1, 2, 3, 4]; example[0]',
output: 'const example = [1, 2, 3, 4]; example.at(0)',
errors: [{
message,
}],
},
{
code: 'const test = [1, 2, 3, 4]; test[1]',
output: 'const test = [1, 2, 3, 4]; test.at(1)',
errors: [{
message,
}],
},
{
code: 'const test = [1, 2, 3, 4]; test[(test.length - 1) / 2]',
output: 'const test = [1, 2, 3, 4]; test.at((test.length - 1) / 2)',
errors: [{
message,
}],
},
{
code: `
const sample = [1, 2, 3, 4];
sample[sample.length - 1]
`,
output: `
const sample = [1, 2, 3, 4];
sample.at(sample.length - 1)
`,
errors: [{
message,
}],
},
{
code: '[0, 1, 2, 3, 4][1]',
output: '[0, 1, 2, 3, 4].at(1)',
errors: [{
message,
}],
},
{
code: 'const index = 1; const a = [1, 2, 3, 4]; a[index]',
output: 'const index = 1; const a = [1, 2, 3, 4]; a.at(index)',
errors: [{
message,
}],
},
{
code: 'const a = [1, 2, 3, 4]; a[a.length - 1]',
output: 'const a = [1, 2, 3, 4]; a.at(a.length - 1)',
errors: [{
message,
}],
},
],
});
10 changes: 10 additions & 0 deletions eslint-plugin-expensify/tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"sourceMap": true
},
"exclude": [
"node_modules"
]
}
Loading

0 comments on commit 88309c5

Please sign in to comment.