-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
build(eslint): add custom eslint rules and eslint rule schematic
BREAKING CHANGE: Switch linting from `tslint` to `eslint`.
- Loading branch information
Showing
52 changed files
with
3,567 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/node_modules | ||
/package-lock.json | ||
/dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"name": "eslint-plugin-ish-custom-rules", | ||
"description": "A plugin with Intershop's custom eslint rules", | ||
"version": "0.0.1", | ||
"main": "dist/index.js", | ||
"files": [ | ||
"dist/**" | ||
], | ||
"scripts": { | ||
"build": "tsc" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
/** | ||
* Checks whether the given class declaration is of type 'Component' | ||
* | ||
* @param node the class declaration, which need to be checked | ||
* @returns the result of the check | ||
*/ | ||
export const isComponent = (node: TSESTree.ClassDeclaration): boolean => isType(node, ['Component']); | ||
|
||
/** | ||
* check the given class declaration to be from given types | ||
* | ||
* @param node the class declaration, which need to be checked | ||
* @param types the wanted types (e.g. 'Component', 'Pipe', ...) | ||
* @returns the result of the check | ||
*/ | ||
export const isType = (node: TSESTree.ClassDeclaration, types: string[]): boolean => { | ||
const decorator = node.decorators?.[0]; | ||
if (!decorator) { | ||
return false; | ||
} | ||
return ( | ||
decorator.expression.type === AST_NODE_TYPES.CallExpression && | ||
decorator.expression.callee.type === AST_NODE_TYPES.Identifier && | ||
types.includes(decorator.expression.callee.name) | ||
); | ||
}; | ||
|
||
/** | ||
* look for the first node in the currently-traversed node, which matches the searched node type | ||
* | ||
* @param context the context for search | ||
* @param type the wanted node type | ||
* @returns the matching node | ||
*/ | ||
export function getClosestAncestorByKind( | ||
context: TSESLint.RuleContext<string, readonly unknown[]>, | ||
type: AST_NODE_TYPES | ||
): TSESTree.Node { | ||
const ancestors = context.getAncestors(); | ||
for (let i = ancestors.length - 1; i >= 0; i--) { | ||
if (ancestors[i].type === type) { | ||
return ancestors[i]; | ||
} | ||
} | ||
} | ||
|
||
export const objectContainsProperty = (node: TSESTree.ObjectExpression, propName: string): boolean => | ||
node.properties.find( | ||
prop => | ||
prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === propName | ||
) !== undefined; | ||
|
||
/** | ||
* normalize path to avoid problems with different operating systems | ||
*/ | ||
export const normalizePath = (filePath: string): string => filePath.replace(/\\/g, '/'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { banImportsFilePatternRule } from './rules/ban-imports-file-pattern'; | ||
import { componentCreationTestRule } from './rules/component-creation-test'; | ||
import { newlineBeforeRootMembersRule } from './rules/newline-before-root-members'; | ||
import { noAssignmentToInputsRule } from './rules/no-assignment-to-inputs'; | ||
import { noCollapsibleIfRule } from './rules/no-collapsible-if'; | ||
import { noInitializeObservablesDirectlyRule } from './rules/no-initialize-observables-directly'; | ||
import { noIntelligenceInArtifactsRule } from './rules/no-intelligence-in-artifacts'; | ||
import { noObjectLiteralTypeAssertionRule } from './rules/no-object-literal-type-assertion'; | ||
import { noReturnUndefinedRule } from './rules/no-return-undefined'; | ||
import { noStarImportsInStoreRule } from './rules/no-star-imports-in-store'; | ||
import { noTestbedWithThenRule } from './rules/no-testbed-with-then'; | ||
import { noVarBeforeReturnRule } from './rules/no-var-before-return'; | ||
import { orderedImportsRule } from './rules/ordered-imports'; | ||
import { privateDestroyFieldRule } from './rules/private-destroy-field'; | ||
import { projectStructureRule } from './rules/project-structure'; | ||
import { useAliasImportsRule } from './rules/use-alias-imports'; | ||
import { useAsyncSynchronizationInTestsRule } from './rules/use-async-synchronization-in-tests'; | ||
import { useCamelCaseEnvironmentPropertiesRule } from './rules/use-camel-case-environment-properties'; | ||
import { useComponentChangeDetectionRule } from './rules/use-component-change-detection'; | ||
import { useJestExtendedMatchersInTestsRule } from './rules/use-jest-extended-matchers-in-tests'; | ||
|
||
const rules = { | ||
'no-return-undefined': noReturnUndefinedRule, | ||
'no-assignment-to-inputs': noAssignmentToInputsRule, | ||
'use-component-change-detection': useComponentChangeDetectionRule, | ||
'no-initialize-observables-directly': noInitializeObservablesDirectlyRule, | ||
'no-intelligence-in-artifacts': noIntelligenceInArtifactsRule, | ||
'use-async-synchronization-in-tests': useAsyncSynchronizationInTestsRule, | ||
'no-testbed-with-then': noTestbedWithThenRule, | ||
'component-creation-test': componentCreationTestRule, | ||
'private-destroy-field': privateDestroyFieldRule, | ||
'use-jest-extended-matchers-in-tests': useJestExtendedMatchersInTestsRule, | ||
'ordered-imports': orderedImportsRule, | ||
'use-camel-case-environment-properties': useCamelCaseEnvironmentPropertiesRule, | ||
'no-star-imports-in-store': noStarImportsInStoreRule, | ||
'use-alias-imports': useAliasImportsRule, | ||
'no-collapsible-if': noCollapsibleIfRule, | ||
'no-object-literal-type-assertion': noObjectLiteralTypeAssertionRule, | ||
'ban-imports-file-pattern': banImportsFilePatternRule, | ||
'project-structure': projectStructureRule, | ||
'newline-before-root-members': newlineBeforeRootMembersRule, | ||
'no-var-before-return': noVarBeforeReturnRule, | ||
}; | ||
|
||
module.exports = { | ||
rules, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
import { normalizePath } from '../helpers'; | ||
|
||
export interface RuleSetting { | ||
filePattern: string; | ||
name: string; | ||
importNamePattern?: string; | ||
starImport?: boolean; | ||
message: string; | ||
} | ||
|
||
/** | ||
* Allows you to specify imports that you don't want to use in files specified by a pattern. | ||
* | ||
* filePattern RegExp pattern to specify the file, which needs to be validated | ||
* name RegExp pattern for the import source (path) | ||
* importNamePattern RegExp pattern for the import specifier | ||
* starImport validate the no import * as from given name are contained | ||
* message error message, which should be displayed, when the validation failed | ||
*/ | ||
export const banImportsFilePatternRule: TSESLint.RuleModule<string, RuleSetting[][]> = { | ||
meta: { | ||
messages: { | ||
banImportsFilePatternError: `{{message}}`, | ||
}, | ||
type: 'problem', | ||
schema: [ | ||
{ | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
properties: { | ||
filePattern: { type: 'string' }, | ||
name: { type: 'string' }, | ||
importNames: { type: 'array' }, | ||
starImport: { type: 'boolean' }, | ||
message: { type: 'string' }, | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
create: context => ({ | ||
ImportDeclaration(node: TSESTree.ImportDeclaration) { | ||
const rules = context.options[0]; | ||
rules.forEach(rule => { | ||
if ( | ||
new RegExp(rule.filePattern).test(normalizePath(context.getFilename())) && | ||
node.source.value === rule.name && | ||
checkValidityOfSpecifiers(node.specifiers, rule.importNamePattern, rule.starImport) | ||
) { | ||
context.report({ | ||
node, | ||
data: { | ||
message: `${rule.message}`, | ||
}, | ||
messageId: 'banImportsFilePatternError', | ||
}); | ||
} else { | ||
return {}; | ||
} | ||
}); | ||
}, | ||
}), | ||
}; | ||
|
||
/** | ||
* validate given rules against the specifiers of the current import declaration | ||
* | ||
* @param specifiers all specifiers of the import declaration | ||
* @param importName the pattern to check | ||
* @param starImport flag to check for star import | ||
* @returns the validity of the specifiers | ||
*/ | ||
const checkValidityOfSpecifiers = ( | ||
specifiers: TSESTree.ImportClause[], | ||
importName: string, | ||
starImport: boolean | ||
): boolean => | ||
(!importName && !starImport) || | ||
(importName && specifiers.some(specifier => new RegExp(importName).test(specifier.local.name))) || | ||
(starImport && specifiers.some(specifier => specifier.type === TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
import { normalizePath } from '../helpers'; | ||
|
||
const SHOULD_BE_CREATED_NAME = 'should be created'; | ||
|
||
/** | ||
* Checks whether component tests contain at least a few standard test cases. | ||
*/ | ||
export const componentCreationTestRule: TSESLint.RuleModule<string, []> = { | ||
meta: { | ||
messages: { | ||
noDescribe: 'All component test files have to include a describe block.', | ||
noCreationTest: `The component does not have an active '${SHOULD_BE_CREATED_NAME}' test`, | ||
noComponentTruthyTest: `'${SHOULD_BE_CREATED_NAME}' block does not test if component is truthy`, | ||
noElementTruthyTest: `'${SHOULD_BE_CREATED_NAME}' block does not test if html element is truthy`, | ||
noFixtureDetectChangesTest: `'${SHOULD_BE_CREATED_NAME}' block does not test if feature.detectChanges does not throw`, | ||
}, | ||
fixable: 'code', | ||
type: 'problem', | ||
schema: [], | ||
}, | ||
create: context => { | ||
// helper functions | ||
function findComponentTruthy(node: TSESTree.Node) { | ||
return ( | ||
context | ||
.getSourceCode() | ||
.getText(node) | ||
.search(/.*component.*toBeTruthy.*/) >= 0 | ||
); | ||
} | ||
function findElementTruthy(node: TSESTree.Node) { | ||
return ( | ||
context | ||
.getSourceCode() | ||
.getText(node) | ||
.search(/.*lement.*toBeTruthy.*/) >= 0 | ||
); | ||
} | ||
function findFixtureDetectChanges(node: TSESTree.Node) { | ||
return ( | ||
context | ||
.getSourceCode() | ||
.getText(node) | ||
.search(/[\s\S]*fixture[\s\S]*detectChanges[\s\S]*not\.toThrow[\s\S]*/) >= 0 | ||
); | ||
} | ||
|
||
if (!(normalizePath(context.getFilename()).search(/.(component|container).spec.ts/) > 0)) { | ||
return {}; | ||
} | ||
|
||
let firstDescribe: TSESTree.CallExpression; | ||
|
||
let creationTestNode: TSESTree.CallExpression; | ||
let hasComponentTruthy = false; | ||
let hasElementTruthy = false; | ||
let hasFixtureDetectChanges = false; | ||
|
||
return { | ||
// find correct 'should be created' test that fulfills requirements | ||
'CallExpression[callee.name="it"]'(node: TSESTree.CallExpression) { | ||
// check if should be created test exists | ||
if ( | ||
node.arguments.filter(arg => arg.type === AST_NODE_TYPES.Literal && arg.value === SHOULD_BE_CREATED_NAME) | ||
.length > 0 | ||
) { | ||
creationTestNode = { ...node }; | ||
// check if test fulfills all requirements | ||
const body = ( | ||
node.arguments.find( | ||
arg => arg.type === AST_NODE_TYPES.ArrowFunctionExpression | ||
) as TSESTree.ArrowFunctionExpression | ||
).body; | ||
if (body.type === AST_NODE_TYPES.BlockStatement) { | ||
hasComponentTruthy = body.body.some(findComponentTruthy); | ||
hasElementTruthy = body.body.some(findElementTruthy); | ||
hasFixtureDetectChanges = body.body.some(findFixtureDetectChanges); | ||
} | ||
} | ||
}, | ||
// find first describe block to display the error at a relevant position | ||
'CallExpression[callee.name="describe"]:exit'(node: TSESTree.CallExpression) { | ||
if (!firstDescribe) { | ||
firstDescribe = node; | ||
} | ||
}, | ||
// report errors after everything has been parsed | ||
'Program:exit'(node: TSESTree.Program) { | ||
// report missing describe block in empty test | ||
if (!firstDescribe) { | ||
context.report({ | ||
node, | ||
messageId: 'noDescribe', | ||
}); | ||
return; | ||
} | ||
// report missing test error at describe | ||
if (!creationTestNode) { | ||
context.report({ | ||
node: firstDescribe.callee, | ||
messageId: 'noCreationTest', | ||
}); | ||
return; | ||
} | ||
// report specific missing statement errors at creationTest | ||
if (!hasComponentTruthy) { | ||
context.report({ | ||
node: creationTestNode.callee, | ||
messageId: 'noComponentTruthyTest', | ||
}); | ||
} | ||
if (!hasElementTruthy) { | ||
context.report({ | ||
node: creationTestNode.callee, | ||
messageId: 'noElementTruthyTest', | ||
}); | ||
} | ||
if (!hasFixtureDetectChanges) { | ||
context.report({ | ||
node: creationTestNode.callee, | ||
messageId: 'noFixtureDetectChangesTest', | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
/** | ||
* Checks whether root members of a typescript file (except for imports) are preceded by an empty line. | ||
*/ | ||
export const newlineBeforeRootMembersRule: TSESLint.RuleModule<string, []> = { | ||
meta: { | ||
messages: { | ||
newLineBeforeRootMembers: `New line missing`, | ||
}, | ||
type: 'problem', | ||
fixable: 'code', | ||
schema: [], | ||
}, | ||
create: context => ({ | ||
'Program:exit'(node: TSESTree.Program) { | ||
const rootMembers = [...node.body]; | ||
for (let i = 0; i < rootMembers.length - 1; i++) { | ||
const current = rootMembers[i]; | ||
|
||
// newline after the last import but not between imports themselves | ||
if (isNewlineException(current) && rootMembers[i + 1].type === current.type) { | ||
continue; | ||
} | ||
|
||
const lines = context.getSourceCode().getLines(); | ||
const currentLastLine = current.loc.end.line; | ||
|
||
if (lines[currentLastLine] === '') { | ||
continue; | ||
} | ||
context.report({ | ||
node: current, | ||
messageId: 'newLineBeforeRootMembers', | ||
fix: fixer => fixer.insertTextAfter(current, '\n'), | ||
}); | ||
} | ||
}, | ||
}), | ||
}; | ||
|
||
function isNewlineException(node: TSESTree.Node) { | ||
return ( | ||
node.type === AST_NODE_TYPES.ImportDeclaration || | ||
node.type === AST_NODE_TYPES.ExportNamedDeclaration || | ||
node.type === AST_NODE_TYPES.ExportDefaultDeclaration || | ||
node.type === AST_NODE_TYPES.ExportAllDeclaration | ||
); | ||
} |
Oops, something went wrong.