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: adding eslint rule for converting toBe(true) to toBeTrue() etc #2911

Merged
merged 6 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,47 @@
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
"plugin:@angular-eslint/ng-cli-compat"
],
"rules": {
"jsdoc/newline-after-description": "off",
"custom-rules/space-before-it-blocks": "error"
"@angular-eslint/component-class-suffix": "off",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't want these rules for *.spec.ts files

"@typescript-eslint/naming-convention": "off",
"@angular-eslint/component-selector": "off",
"@angular-eslint/directive-selector": "off",
"indent": "off",
"semi": "off",
"no-underscore-dangle": "off",
"@angular-eslint/template/no-negated-async": "off",
"@typescript-eslint/prefer-for-of": "off",
"prefer-arrow/prefer-arrow-functions": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-assertions": "off",
"@angular-eslint/no-conflicting-lifecycle": "off",
"lines-between-class-members": "off",
"@typescript-eslint/no-shadow": "off",
"complexity": "off",
"max-depth": "off",
"max-params-no-constructor/max-params-no-constructor": "off",
"max-len": "off",
"space-before-function-paren": "off",
"@typescript-eslint/quotes": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-inferrable-types": "off",
"no-unused-expressions": "off",
"custom-rules/space-before-it-blocks": "error",
"custom-rules/prefer-jasmine-matchers": "error",
"custom-rules/prefer-resolve-to-reject-with": "error"
}
}
]
Expand Down
2 changes: 2 additions & 0 deletions eslint-custom-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ module.exports = {
rules: {
'prefer-deep-freeze': require('./rules/eslint-plugin-prefer-deep-freeze'),
'space-before-it-blocks': require('./rules/eslint-plugin-space-before-it-blocks'),
'prefer-jasmine-matchers': require('./rules/eslint-plugin-prefer-jasmine-matchers'),
'prefer-resolve-to-reject-with': require('./rules/eslint-plugin-prefer-resolve-to-reject-with')
},
};
134 changes: 134 additions & 0 deletions eslint-custom-rules/rules/eslint-plugin-prefer-jasmine-matchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Use toBeNull() instead of toBe(null) or toEqual(null)
// Use toBeUndefined() instead of toBe(undefined) or toEqual(undefined)
// Use toBeTrue() instead of toBe(true) or toEqual(true)
// Use toBeFalse() instead of toBe(false) or toEqual(false)
// Use toBeNaN() instead of toBe(NaN) or toEqual(NaN)

module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Enforce using toBeTrue(), toBeFalse(), toBeNull(), and toBeUndefined() instead of toBe() or toEqual() with true, false, null, or undefined",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
},

create: function (context) {
return {
CallExpression(node) {
const isToBe = node.callee.property && node.callee.property.name === "toBe";
const isToEqual = node.callee.property && node.callee.property.name === "toEqual";

if ((isToBe || isToEqual) && node.arguments.length === 1) {
const argValue = node.arguments[0].value;
if (argValue === true) {
context.report({
node,
message: "Prefer using toBeTrue() instead of toBe(true) or toEqual(true)",
fix: function (fixer) {
const replacementText = 'toBeTrue()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);

// Find the correct part of the code to replace
const match = nodeText.match(/\.toBe\((true)\)|\.toEqual\((true)\)/);
if (match) {
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 6; // Adjust end position
return fixer.replaceTextRange([start, end], replacementText);
}

return null; // No match found, no fix needed
}
});
} else if (argValue === false) {
context.report({
node,
message: "Prefer using toBeFalse() instead of toBe(false) or toEqual(false)",
fix: function (fixer) {
const replacementText = 'toBeFalse()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);

// Find the correct part of the code to replace
const match = nodeText.match(/\.toBe\((false)\)|\.toEqual\((false)\)/);
if (match) {
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 7; // Adjust end position
return fixer.replaceTextRange([start, end], replacementText);
}

return null; // No match found, no fix needed
}
});
} else if (argValue === null) {
context.report({
node,
message: "Prefer using toBeNull() instead of toBe(null) or toEqual(null)",
fix: function (fixer) {
const replacementText = 'toBeNull()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);

// Find the correct part of the code to replace
const match = nodeText.match(/\.toBe\((null)\)|\.toEqual\((null)\)/);
if (match) {
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 6; // Adjust end position
return fixer.replaceTextRange([start, end], replacementText);
}

return null; // No match found, no fix needed
}
});
} else if (argValue === undefined && node.arguments[0].name === 'undefined') {
// Comparing the name of the argument to undefined since every variable is undefined by default in AST
context.report({
node,
message: "Prefer using toBeUndefined() instead of toBe(undefined) or toEqual(undefined)",
fix: function (fixer) {
const replacementText = 'toBeUndefined()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);

// Find the correct part of the code to replace
const match = nodeText.match(/\.toBe\((undefined)\)|\.toEqual\((undefined)\)/);
if (match) {
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 11; // Adjust end position
return fixer.replaceTextRange([start, end], replacementText);
}

return null; // No match found, no fix needed
}
});
} else if (node.arguments[0].name === 'NaN') {
// Since NaN === NaN returns false, we are comparing the name of the argument to NaN
context.report({
node,
message: "Prefer using toBeNaN() instead of toBe(NaN) or toEqual(NaN)",
fix: function (fixer) {
const replacementText = 'toBeNaN()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);

// Find the correct part of the code to replace
const match = nodeText.match(/\.toBe\((NaN)\)|\.toEqual\((NaN)\)/);
if (match) {
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 5; // Adjust end position
return fixer.replaceTextRange([start, end], replacementText);
}

return null; // No match found, no fix needed
}
});
}
}
}
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const SpyStrategyCall = `CallExpression:matches(
[callee.object.property.name=and],
[callee.object.callee.property.name=withArgs]
)`.replace(/\s+/g, ' ');

const ReturnStrategy = `${SpyStrategyCall}[callee.property.name=returnValue]`;

// Matches Promise.{resolve,reject}(X)
const PromiseCall = 'CallExpression[callee.object.name=Promise]';
const SettledPromise = `${PromiseCall}[callee.property.name=/resolve|reject/]`;

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: "Enforce using resolveTo() and rejectWith() instead of Promise.resolve() and Promise.reject() in Jasmine tests",
category: "Best Practices",
recommended: true,
},
fixable: 'code',
schema: []
},

create: context => ({
[`${ReturnStrategy} > ${SettledPromise}.arguments:first-child`] (promiseCall) {
const returnStrategyCall = promiseCall.parent;
const returnValueMethod = returnStrategyCall.callee.property;
const preferredMethod = promiseCall.callee.property.name === 'resolve'
? 'resolveTo' : 'rejectWith';

context.report({
message: `Prefer ${preferredMethod}`,
loc: {
start: returnValueMethod.loc.start,
end: returnStrategyCall.loc.end
},
fix (fixer) {
const code = context.getSourceCode();
return [
// Replace Promise constructor call with its arguments
fixer.remove(promiseCall.callee),
fixer.remove(code.getTokenAfter(promiseCall.callee)),
fixer.remove(code.getLastToken(promiseCall)),

// Replace returnValue method with resolveTo or rejectWith
fixer.replaceText(returnValueMethod, preferredMethod)
];
}
})
}
})
};
6 changes: 4 additions & 2 deletions src/app/core/mock-data/account-option.data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import deepFreeze from 'deep-freeze-strict';

import { AccountOption } from '../models/account-option.model';
import { multiplePaymentModesData } from '../test-data/accounts.service.spec.data';

export const accountOptionData1: AccountOption[] = [
export const accountOptionData1: AccountOption[] = deepFreeze([
{
label: 'account1',
value: multiplePaymentModesData[0],
Expand All @@ -10,4 +12,4 @@ export const accountOptionData1: AccountOption[] = [
label: 'account2',
value: multiplePaymentModesData[1],
},
];
]);
10 changes: 6 additions & 4 deletions src/app/core/mock-data/acess-token-data.data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import deepFreeze from 'deep-freeze-strict';

import { AccessTokenData } from '../models/access-token-data.model';

export const apiAccessTokenRes: AccessTokenData = {
export const apiAccessTokenRes: AccessTokenData = deepFreeze({
iat: 1678349549,
iss: 'FyleApp',
user_id: 'usvKA4X8Ugcr',
Expand All @@ -12,9 +14,9 @@ export const apiAccessTokenRes: AccessTokenData = {
version: '3',
cluster_domain: '"https://staging.fyle.tech"',
exp: 1678353149,
};
});

export const apiTokenWithoutRoles: AccessTokenData = {
export const apiTokenWithoutRoles: AccessTokenData = deepFreeze({
iat: 1678349549,
iss: 'FyleApp',
user_id: 'usvKA4X8Ugcr',
Expand All @@ -25,4 +27,4 @@ export const apiTokenWithoutRoles: AccessTokenData = {
version: '3',
cluster_domain: '"https://staging.fyle.tech"',
exp: 1678353149,
};
});
18 changes: 10 additions & 8 deletions src/app/core/mock-data/action-sheet-options.data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const actionSheetOptionsData = [
import deepFreeze from 'deep-freeze-strict';

export const actionSheetOptionsData = deepFreeze([
{
text: 'Split Expense By Category',
handler: () => {},
Expand All @@ -19,8 +21,8 @@ export const actionSheetOptionsData = [
text: 'Remove Card Expense',
handler: () => {},
},
];
export const expectedActionSheetButtonRes = [
]);
export const expectedActionSheetButtonRes = deepFreeze([
{
text: 'Capture Receipt',
icon: 'assets/svg/camera.svg',
Expand All @@ -45,9 +47,9 @@ export const expectedActionSheetButtonRes = [
cssClass: 'capture-receipt',
handler: undefined,
},
];
]);

export const expectedActionSheetButtonsWithMileage = [
export const expectedActionSheetButtonsWithMileage = deepFreeze([
{
text: 'Capture Receipt',
icon: 'assets/svg/camera.svg',
Expand All @@ -66,9 +68,9 @@ export const expectedActionSheetButtonsWithMileage = [
cssClass: 'capture-receipt',
handler: undefined,
},
];
]);

export const expectedActionSheetButtonsWithPerDiem = [
export const expectedActionSheetButtonsWithPerDiem = deepFreeze([
{
text: 'Capture Receipt',
icon: 'assets/svg/camera.svg',
Expand All @@ -87,4 +89,4 @@ export const expectedActionSheetButtonsWithPerDiem = [
cssClass: 'capture-receipt',
handler: undefined,
},
];
]);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import deepFreeze from 'deep-freeze-strict';

import { AddEditAdvanceRequestFormValue } from '../models/add-edit-advance-request-form-value.model';
import { recentlyUsedProjectRes } from './recently-used.data';

export const addEditAdvanceRequestFormValueData: AddEditAdvanceRequestFormValue = {
export const addEditAdvanceRequestFormValueData: AddEditAdvanceRequestFormValue = deepFreeze({
currencyObj: {
amount: 130,
currency: 'USD',
Expand All @@ -12,17 +14,17 @@ export const addEditAdvanceRequestFormValueData: AddEditAdvanceRequestFormValue
notes: 'Test notes',
project: null,
customFieldValues: null,
};
});

export const addEditAdvanceRequestFormValueData2: AddEditAdvanceRequestFormValue = {
export const addEditAdvanceRequestFormValueData2: AddEditAdvanceRequestFormValue = deepFreeze({
currencyObj: null,
purpose: null,
notes: null,
project: null,
customFieldValues: [],
};
});

export const addEditAdvanceRequestFormValueData3: AddEditAdvanceRequestFormValue = {
export const addEditAdvanceRequestFormValueData3: AddEditAdvanceRequestFormValue = deepFreeze({
...addEditAdvanceRequestFormValueData,
project: recentlyUsedProjectRes[0],
};
});
6 changes: 4 additions & 2 deletions src/app/core/mock-data/advance-platform.data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import deepFreeze from 'deep-freeze-strict';

import { CustomFieldTypes } from '../enums/platform/v1/custom-fields-type.enum';
import { AdvancesPlatform } from '../models/platform/advances-platform.model';
import { PlatformApiResponse } from '../models/platform/platform-api-response.model';

export const advancePlatform: PlatformApiResponse<AdvancesPlatform> = {
export const advancePlatform: PlatformApiResponse<AdvancesPlatform> = deepFreeze({
count: 1,
offset: 0,
data: [
Expand Down Expand Up @@ -85,4 +87,4 @@ export const advancePlatform: PlatformApiResponse<AdvancesPlatform> = {
},
},
],
};
});
Loading
Loading