Skip to content

Commit

Permalink
Merge pull request #119 from Expensify/Rory-SetupCITests
Browse files Browse the repository at this point in the history
Setup ci tests
  • Loading branch information
carlosmiceli authored Oct 3, 2024
2 parents 0b2a9c5 + d45e06d commit 1744186
Show file tree
Hide file tree
Showing 14 changed files with 274 additions and 189 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
extends: './index.js',
}
};
34 changes: 34 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: ESLint

on:
pull_request:
types: [opened, synchronize]

concurrency:
group: eslint-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Run npm install
if: steps.cache-node-modules.outputs.cache-hit != 'true'
uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
with:
timeout_minutes: 30
max_attempts: 3
command: npm ci

- name: Run ESLint
run: npm run lint
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Jest unit tests

on:
pull_request:
types: [opened, synchronize]

concurrency:
group: jest-${{ github.ref }}
cancel-in-progress: true

jobs:
jest:
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Run npm install
if: steps.cache-node-modules.outputs.cache-hit != 'true'
uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
with:
timeout_minutes: 30
max_attempts: 3
command: npm ci

- name: Run Jest tests
run: npm run test
68 changes: 37 additions & 31 deletions eslint-plugin-expensify/prefer-at.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { AST_NODE_TYPES, ESLintUtils } = require('@typescript-eslint/utils');
const {AST_NODE_TYPES, ESLintUtils} = require('@typescript-eslint/utils');
const message = require('./CONST').MESSAGE.PREFER_AT;

const { isLeftHandSide } = require('./utils/is-left-hand-side');
const {isLeftHandSide} = require('./utils/is-left-hand-side');

module.exports = {
meta: {
Expand Down Expand Up @@ -34,14 +34,17 @@ module.exports = {
return null;

case AST_NODE_TYPES.BinaryExpression:
// eslint-disable-next-line no-case-declarations
const left = parseExpression(node.left);
// eslint-disable-next-line no-case-declarations
const right = parseExpression(node.right);
if (left !== null && right !== null) {
return `(${left} ${node.operator} ${right})`;
}
return null;

case AST_NODE_TYPES.UnaryExpression:
// eslint-disable-next-line no-case-declarations
const argument = parseExpression(node.argument);
if (argument !== null) {
return `${node.operator}${argument}`;
Expand Down Expand Up @@ -70,49 +73,52 @@ module.exports = {
}

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

// Skip if the property is a method (like a?.map)
if (node.parent && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) {
return;
}
if (!isArrayType(node.object)) {
return;
}

// Skip if the node is part of an assignment expression
if (isLeftHandSide(node)) {
return;
}
// Skip if the property is a method (like a?.map)
if (node.parent && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) {
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)})`);
},
});
}
// Skip if the node is part of an assignment expression
if (isLeftHandSide(node)) {
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
node.parent
&& node.parent.type === AST_NODE_TYPES.MemberExpression
&& node.parent.property === node
);
}

return {
MemberExpression(node) {
if (!shouldIgnoreNode(node)) {
checkNode(node);
if (shouldIgnoreNode(node)) {
return;
}
checkNode(node);
},
};
},
Expand Down
17 changes: 9 additions & 8 deletions eslint-plugin-expensify/prefer-type-fest.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ const rule = {
return {
Program(node) {
// Find type-fest import declarations
node.body.forEach(statement => {
if (statement.type === 'ImportDeclaration' && statement.source.value === 'type-fest') {
node.body.forEach((statement) => {
if (statement.type !== 'ImportDeclaration' || statement.source.value !== 'type-fest') {
return;
}
typeFestImport = statement;
}
});
},
},
TSIndexedAccessType(node) {
const objectType = node.objectType;
const indexType = node.indexType;
Expand Down Expand Up @@ -54,7 +55,7 @@ const rule = {
const fixes = [fixer.replaceText(node, `ValueOf<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'ValueOf', 'type-fest', true));
return fixes;
}
},
});
}
}
Expand All @@ -68,7 +69,7 @@ const rule = {
const fixes = [fixer.replaceText(node, `TupleToUnion<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'TupleToUnion', 'type-fest', true));
return fixes;
}
},
});
}
}
Expand All @@ -89,7 +90,7 @@ const rule = {
const fixes = [fixer.replaceText(node, `ValueOf<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'ValueOf', 'type-fest', true));
return fixes;
}
},
});
}
}
Expand All @@ -103,7 +104,7 @@ const rule = {
const fixes = [fixer.replaceText(node, `TupleToUnion<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'TupleToUnion', 'type-fest', true));
return fixes;
}
},
});
}
}
Expand Down
5 changes: 5 additions & 0 deletions eslint-plugin-expensify/tests/prefer-type-fest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,21 @@ ruleTester.run('prefer-type-fest', rule, {
code: 'const CONST = { VIDEO_PLAYER: { PLAYBACK_SPEEDS: [0.25, 0.5, 1, 1.5, 2] } } as const; type Bad = (typeof CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS)[number];',
errors: [{message: PREFER_TYPE_FEST_TUPLE_TO_UNION}],
parser: require.resolve('@typescript-eslint/parser'),
// eslint-disable-next-line max-len
output: 'import type {TupleToUnion} from \'type-fest\';\nconst CONST = { VIDEO_PLAYER: { PLAYBACK_SPEEDS: [0.25, 0.5, 1, 1.5, 2] } } as const; type Bad = TupleToUnion<typeof CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS>;',
},
{
code: 'const TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, (typeof TIMEZONES)[number]> = { a: \'a\', b: \'b\' };',
errors: [{message: PREFER_TYPE_FEST_TUPLE_TO_UNION}],
parser: require.resolve('@typescript-eslint/parser'),
// eslint-disable-next-line max-len
output: 'import type {TupleToUnion} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, TupleToUnion<typeof TIMEZONES>> = { a: \'a\', b: \'b\' };',
},
{
code: 'import type {Something} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, (typeof TIMEZONES)[number]> = { a: \'a\', b: \'b\' };',
errors: [{message: PREFER_TYPE_FEST_TUPLE_TO_UNION}],
parser: require.resolve('@typescript-eslint/parser'),
// eslint-disable-next-line max-len
output: 'import type {Something, TupleToUnion} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, TupleToUnion<typeof TIMEZONES>> = { a: \'a\', b: \'b\' };',
},
{
Expand All @@ -76,6 +79,7 @@ ruleTester.run('prefer-type-fest', rule, {
code: 'const CONST = { AVATAR_SIZE: { SMALL: \'small\', MEDIUM: \'medium\', LARGE: \'large\' } } as const; type Bad = { avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; }',
errors: [{message: PREFER_TYPE_FEST_VALUE_OF}],
parser: require.resolve('@typescript-eslint/parser'),
// eslint-disable-next-line max-len
output: 'import type {ValueOf} from \'type-fest\';\nconst CONST = { AVATAR_SIZE: { SMALL: \'small\', MEDIUM: \'medium\', LARGE: \'large\' } } as const; type Bad = { avatarSize?: ValueOf<typeof CONST.AVATAR_SIZE>; }',
},
{
Expand All @@ -94,6 +98,7 @@ ruleTester.run('prefer-type-fest', rule, {
code: 'import somethingElse from \'something-else\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = (typeof COLORS)[keyof COLORS];',
errors: [{message: PREFER_TYPE_FEST_VALUE_OF}],
parser: require.resolve('@typescript-eslint/parser'),
// eslint-disable-next-line max-len
output: 'import type {ValueOf} from \'type-fest\';\nimport somethingElse from \'something-else\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = ValueOf<typeof COLORS>;',
},
],
Expand Down
54 changes: 42 additions & 12 deletions eslint-plugin-expensify/tests/use-periods-error-messages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,39 @@ const ruleTester = new RuleTester({
});

const goodExampleSingleSentence = `
error: {
testMessage: 'This is a test message'
}
const err = new Error('This is a test message');
`;

const goodExampleMultipleSentences = `
error: {
testMessage: 'This is a test message. Last period is mandatory.'
}
const err = new Error('This is a test message. Last period is mandatory.');
`;

const goodExampleSingleSentenceWithVar = `
const errorMessage = 'This is a test message';
const err = new Error(errorMessage);
`;

const goodExampleMultipleSentencesWithVar = `
const errorMessage = 'This is a test message. Last period is mandatory.';
const err = new Error(errorMessage);
`;

const badExampleSingleSentence = `
error: {
testMessage: 'This is a test message.'
}
const err = new Error('This is a test message.');
`;

const badExampleSingleSentenceWithVar = `
const errorMessage = 'This is a test message.';
const err = new Error(errorMessage);
`;

const badExampleMultipleSentences = `
error: {
testMessage: 'This is a test message. Last period is mandatory'
}
const err = new Error('This is a test message. Last period is mandatory');
`;

const badExampleMultipleSentencesWithVar = `
const errorMessage = 'This is a test message. Last period is mandatory';
const err = new Error(errorMessage);
`;

ruleTester.run('use-periods-for-error-messages', rule, {
Expand All @@ -41,6 +53,12 @@ ruleTester.run('use-periods-for-error-messages', rule, {
{
code: goodExampleMultipleSentences,
},
{
code: goodExampleSingleSentenceWithVar,
},
{
code: goodExampleMultipleSentencesWithVar,
},
],
invalid: [
{
Expand All @@ -49,11 +67,23 @@ ruleTester.run('use-periods-for-error-messages', rule, {
message,
}],
},
{
code: badExampleSingleSentenceWithVar,
errors: [{
message,
}],
},
{
code: badExampleMultipleSentences,
errors: [{
message,
}],
},
{
code: badExampleMultipleSentencesWithVar,
errors: [{
message,
}],
},
],
});
Loading

0 comments on commit 1744186

Please sign in to comment.