Skip to content

Commit

Permalink
feat: adding eslint method for deep freeze (#2904)
Browse files Browse the repository at this point in the history
* feat: adding eslint method for deep freeze - iteration 1

* minor

* if we already imported deepFreeze dont import again

* one minor issue with array mock data

* revert prefer-jasmine-matchers'

* feat: adding eslint rule for using space between it blocks (#2909)

* feat: adding eslint rule for using space between it blocks

* minor

* ignore eslint rules file from eslint lol

* new method for this eslint rule

* feat: adding eslint rule for converting toBe(true) to toBeTrue() etc (#2911)

* feat: adding eslint rule for converting toBe(true) to toBeTrue() etc

* minor

* minor

* feat: prefer resolveTo and rejectWith instead of returnValue(Promise.resolve()) (#2912)

* feat: prefer resolveTo and rejectWith instead of returnValue(Promise.resolve())

* feat: added deepFreeze to not pollute global mock data (#2913)

* feat: added deepFreeze to not pollute global mock data

* feat: added deepFreeze to not pollute global mock data - Part 2 (#2914)

* feat: added deepFreeze to not pollute global mock data - Part 2

* feat: added deepFreeze to not pollute global mock data - Part 3 (#2915)

* feat: added deepFreeze to not pollute global mock data - Part 3

* feat: added deepFreeze to not pollute global mock data - Part 4 (#2916)

* feat: added deepFreeze to not pollute global mock data - Part 4

* feat: added deepFreeze to not pollute global mock data - Part 5 (#2917)

* feat: added deepFreeze to not pollute global mock data - Part 5

* feat: added deepFreeze to not pollute global mock data - Part 6 (#2918)

* feat: added deepFreeze to not pollute global mock data - Part 6

* feat: added deepFreeze to not pollute global mock data - Part 7 (#2919)

* feat: added deepFreeze to not pollute global mock data - Part 7

* minor

* feat: added deepFreeze to not pollute global mock data - Part 8 (#2922)

* feat: added deepFreeze to not pollute global mock data - Part 8

* feat: added deepFreeze to not pollute global mock data - Part 9 (#2924)

* feat: added deepFreeze to not pollute global mock data - Part 9

* feat: added deepFreeze to not pollute global mock data - Part 10 (#2925)

* feat: added deepFreeze to not pollute global mock data - Part 10

* feat: added deepFreeze to not pollute global mock data - Part 11 (#2927)

* feat: added deepFreeze to not pollute global mock data - Part 11

* feat: added deepFreeze to not pollute global mock data - Part 12 (#2929)

* feat: added deepFreeze to not pollute global mock data - Part 12

* feat: added deepFreeze to not pollute global mock data - Part 13 (#2930)

* feat: added deepFreeze to not pollute global mock data - Part 13

* minor

* feat: added deepFreeze to not pollute global mock data - Part 14 (#2933)

* feat: added deepFreeze to not pollute global mock data - Part 14

* feat: added deepFreeze to not pollute global mock data - Part 15 (#2934)

* feat: added deepFreeze to not pollute global mock data - Part 15

* test: fixing tests - part 1 (#2939)

* test: fixing tests - part 1

* test: fixing tests - part 2 (#2940)

* test: fixing tests - part 2

* test: fixing tests - part 3 (#2941)

* test: fixing tests - part 3

* test: fixing tests - part 4 (#2943)

* test: fixing tests - part 4

* test: fixing tests - part 5 (#2945)

* minor
  • Loading branch information
suyashpatil78 authored May 4, 2024
1 parent 1548728 commit 0cc9da8
Show file tree
Hide file tree
Showing 268 changed files with 4,076 additions and 3,091 deletions.
61 changes: 59 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
"zone-flags.ts",
"test.ts",
"main.ts",
"*.spec.ts"
"eslint-custom-rules/**/*"
],
"plugins": ["max-params-no-constructor"],
"plugins": ["max-params-no-constructor", "custom-rules"],
"overrides": [
{
"files": ["*.ts"],
"excludedFiles": ["*.spec.ts", "*.e2e-spec.ts", "*.po.ts"],
"parserOptions": {
"project": ["tsconfig.json", "e2e/tsconfig.json"],
"createDefaultProgram": true
Expand Down Expand Up @@ -89,6 +90,62 @@
"rules": {
"@angular-eslint/template/no-negated-async": "off"
}
},
{
"files": ["*.data.ts"],
"rules": {
"custom-rules/prefer-deep-freeze": "error"
}
},
{
"files": ["*.spec.ts"],
"parserOptions": {
"project": ["tsconfig.json", "e2e/tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat"
],
"rules": {
"jsdoc/newline-after-description": "off",
"@angular-eslint/component-class-suffix": "off",
"@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"
}
}
]
}
8 changes: 8 additions & 0 deletions eslint-custom-rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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')
},
};
65 changes: 65 additions & 0 deletions eslint-custom-rules/rules/eslint-plugin-prefer-deep-freeze.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce using deep-freeze-strict on all variables in *.data.ts files to avoid flaky tests',
category: 'Best Practices',
recommended: true,
},
fixable: 'code',
},
create: function(context) {
return {
VariableDeclarator(node) {
const sourceCode = context.getSourceCode();
const filename = context.getFilename();

if (filename.endsWith('.data.ts')) {
const { id, init } = node;

if (id && init && (init.type === 'ObjectExpression' || init.type === 'ArrayExpression')) {
const fix = (fixer) => {
const variableText = sourceCode.getText(init);
const start = init.range[0];
const end = init.range[1];

const fixes = [];

// Check if deepFreeze is already imported
const importNodes = sourceCode.ast.body.filter(
(node) => node.type === 'ImportDeclaration' && node.source.value === 'deep-freeze-strict'
);
if (importNodes.length === 0) {
// Add import statement at the top if not imported
fixes.push(
fixer.insertTextBeforeRange(
[sourceCode.ast.range[0], sourceCode.ast.range[0]],
"import deepFreeze from 'deep-freeze-strict';\n\n"
)
);
}

// Wrap the object or array with deepFreeze method
fixes.push(
fixer.replaceTextRange(
[start, end],
`deepFreeze(${variableText})`
)
);

return fixes;
};

if (init.type === 'ObjectExpression' || init.type === 'ArrayExpression') {
context.report({
node,
message: `Use deep-freeze-strict function on variable "${id.name}" to avoid flaky tests`,
fix,
});
}
}
}
},
};
},
};
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)
];
}
})
}
})
};
53 changes: 53 additions & 0 deletions eslint-custom-rules/rules/eslint-plugin-space-before-it-blocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
var suiteRegexp = /^(f|x)?describe$/;

module.exports = {
meta: {
type: 'layout',
docs: {
description: 'Enforce a space before each `it` block in spec.ts files',
category: 'Stylistic Issues',
recommended: true,
},
fixable: 'code',
},
create: function (context) {
return {
CallExpression: function (node) {
var declWithoutPadding = null;
if (suiteRegexp.test(node.callee.name)) {
var declarations = getDescribeDeclarationsContent(node);
declarations.forEach((decl, i) => {
var next = declarations[i + 1];
if (next && !(next.loc.start.line - decl.loc.end.line >= 2)) {
declWithoutPadding = decl;
}
})
}
if (declWithoutPadding) {
context.report({
message: 'Use new line between declarations for more readability',
node,
loc: node.loc,
fix (fixer) {
return fixer.insertTextAfter(declWithoutPadding, '\n');
},
})
}
}
}
}
}

function getDescribeDeclarationsContent (describe) {
var declartionsRegexp = /^(((before|after)(Each|All))|^(f|x)?(it|describe))$/;
var declarations = [];
if (describe.arguments && describe.arguments[1] && describe.arguments[1].body && describe.arguments[1].body.body) {
var content = describe.arguments[1].body.body;
content.forEach(node => {
if (node.type === 'ExpressionStatement' && node.expression.callee && declartionsRegexp.test(node.expression.callee.name)) {
declarations.push(node);
}
})
}
return declarations;
}
Loading

0 comments on commit 0cc9da8

Please sign in to comment.