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) Add rule to limit maximum indentation #16

Merged
merged 1 commit into from
Nov 23, 2022
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
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests (Windows)",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module.exports = {
| [unregister-events](/docs/unregister-events.md) | ✅ | | Ensures all events registered in React components are unregistered when component unmounts |
| [no-unstable-dependencies](/docs/no-unstable-dependencies.md) | ✅ | | Helps find dependencies that are used in React hook dependency arrays that will change values every time the component render is called |
| [require-state-property-definition](/docs/require-state-property-definition.md) | ✅ | | Check for expected/unexpected state definitions in class components |

| [max-indentation](/docs/max-indentation.md) | | | prevents code blocks from exceeded a specified number of indents |
## LICENSE

MIT
57 changes: 57 additions & 0 deletions docs/max-indentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Limit levels of indentation (`max-indentation`)

This rule prevents code blocks from exceeded a specified number of indents. This plugin can be useful in conjunction with plugins like Prettier, where once a certain number of indents are reached, code becomes nearly unreadable. Code blocks with multiple layers of indentation can be a code smell of large functions and cyclomatic complexity that could be broken up into smaller functions.

## Rule Details

Examples of **incorrect** code for this rule, assuming a maximum indentation of 4 levels:

```typescript
const myFunction = () => {
if (Math.random() > 0.5) {
const myInnerFunction = () => {
if (Math.random() > 0.5) {
return [
// These lines should fail given that there are more than 4 indentation levels
1,
2,
3,
4,
]
}
}
}
}
```

Examples of **correct** code for this rule, again assuming a maximum indentation of 4 levels:

```typescript
const otherFunction = () => {
return [
// These lines should fail given that there are more than 4 indentation levels
1,
2,
3,
4,
];
}

const myFunction = () => {
if (Math.random() > 0.5) {
const myInnerFunction = () => {
if (Math.random() > 0.5) {
otherFunction();
}
}
}
}
```

## When Not To Use It

May not be recommended without an opinionated code formatter like Prettier. Without an opinionated formatter, developers may instead be encouraged to write more one liners, which is not a recommended fix for this issue.

## Auto-fixable?

No ❌
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import privateComponentMethods from "./rules/private-component-methods";
import requireStatePropertyDefinition from "./rules/require-state-property-definition";
import unregisterEvents from "./rules/unregister-events";
import noUnstableDependencies from "./rules/no-unstable-dependencies";
import maxIndentation from "./rules/max-indentation";

export = {
rules: {
Expand All @@ -13,6 +14,7 @@ export = {
"unregister-events": unregisterEvents,
"no-unstable-dependencies": noUnstableDependencies,
"require-state-property-definition": requireStatePropertyDefinition,
"max-indentation": maxIndentation,
},
configs: {
recommended: {
Expand All @@ -24,6 +26,7 @@ export = {
"enterprise-extras/unregister-events": "error",
"enterprise-extras/no-unstable-dependencies": "warn",
"enterprise-extras/require-state-property-definition": "warn",
"enterprise-extras/max-indentation": "warn",
},
},
all: {
Expand All @@ -35,6 +38,7 @@ export = {
"enterprise-extras/unregister-events": "error",
"enterprise-extras/no-unstable-dependencies": "error",
"enterprise-extras/require-state-property-definition": "error",
"enterprise-extras/max-indentation": "error",
},
},
},
Expand Down
191 changes: 191 additions & 0 deletions src/rules/max-indentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
import { RuleListener } from "@typescript-eslint/utils/dist/ts-eslint";
import { isLiteral } from "../utils/type-guards";

type MessageIds = "maxIndentationExceeded";
type Options = [
number,
{ includeSpaces: boolean; includeTab: boolean; spacesPerTab: number }
];

export default ESLintUtils.RuleCreator(
(name) =>
`https://github.com/buildertrend/eslint-plugin-enterprise-extras/blob/main/docs/${name}.md`
)<Options, MessageIds>({
name: "max-indentation",
meta: {
type: "layout",
docs: {
recommended: false,
description:
"Limits the maximum number of times a line can be indented to improve readability. Especially useful in conjunction with other code formatting tools like Prettier.",
},
messages: {
maxIndentationExceeded:
"Maximum indentation exceeded. Try breaking out the code into functions to improve readability",
},
schema: [
{ type: "number", default: 5, minimum: 1 },
{
type: "object",
properties: {
includeSpaces: {
type: "boolean",
default: true,
},
includeTab: {
type: "boolean",
default: true,
},
spacesPerTab: {
type: "number",
minimum: 1,
default: 4,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
5,
{
includeSpaces: true,
includeTab: true,
spacesPerTab: 4,
},
],
create: function (context) {
type ReportDescriptor = Parameters<typeof context.report>[0];
type ErrorReport = Omit<ReportDescriptor, "loc"> & {
loc: TSESTree.SourceLocation;
} & { node: TSESTree.Node };

const maxNumberOfIndentation = context.options[0];
const { includeSpaces, includeTab, spacesPerTab } = context.options[1];

const groupExp: string[] = [];
if (includeSpaces) {
groupExp.push(`( {${spacesPerTab}})`);
}
if (includeTab) {
groupExp.push(`(\\t)`);
}
const combinedExp = groupExp.join("|");
const maxWhitespaceRegExp = new RegExp(
`(${combinedExp}){${maxNumberOfIndentation + 1},}`
);

// Module store of errors that we have found
let errors: ErrorReport[] = [];

const sourceCode = context.getSourceCode();
const commentNodes = sourceCode.getAllComments();

function removeWhitespaceError(node: TSESTree.Node) {
const locStart = node.loc.start;
const locEnd = node.loc.end;

errors = errors.filter(
({ loc: { start: errorLocStart } }) =>
errorLocStart.line < locStart.line ||
(errorLocStart.line === locStart.line &&
errorLocStart.column < locStart.column) ||
(errorLocStart.line === locEnd.line &&
errorLocStart.column >= locEnd.column) ||
errorLocStart.line > locEnd.line
);
}

function removeInvalidNodeErrorsInLiteral(node: TSESTree.Literal) {
const shouldCheckStrings =
isLiteral(node) && typeof node.value === "string";
const shouldCheckRegExps = Boolean(
(node as TSESTree.RegExpLiteral).regex
);

if (shouldCheckStrings || shouldCheckRegExps) {
// If we have irregular characters remove them from the errors list
if (maxWhitespaceRegExp.test(node.raw)) {
removeWhitespaceError(node);
}
}
}

function removeInvalidNodeErrorsInTemplateLiteral(
node: TSESTree.TemplateElement
) {
if (typeof node.value.raw === "string") {
if (maxWhitespaceRegExp.test(node.value.raw)) {
removeWhitespaceError(node);
}
}
}

function removeInvalidNodeErrorsInComment(node) {
if (maxWhitespaceRegExp.test(node.value)) {
removeWhitespaceError(node);
}
}

function checkForMaxIndentation(node: TSESTree.Node) {
const sourceLines = sourceCode.lines;

sourceLines.forEach((sourceLine, lineIndex) => {
const lineNumber = lineIndex + 1;
const match = maxWhitespaceRegExp.exec(sourceLine);
if (match) {
errors.push({
node,
messageId: "maxIndentationExceeded",
loc: {
start: {
line: lineNumber,
column: match.index,
},
end: {
line: lineNumber,
column: match.index + match[0].length,
},
},
});
}
});
}

const nodes: RuleListener = {};

if (
(includeTab || includeSpaces) &&
maxWhitespaceRegExp.test(sourceCode.getText())
) {
nodes.Program = function (node) {
/*
* As we can easily fire warnings for all white space issues with
* all the source its simpler to fire them here.
* This means we can check all the application code without having
* to worry about issues caused in the parser tokens.
* When writing this code also evaluating per node was missing out
* connecting tokens in some cases.
* We can later filter the errors when they are found to be not an
* issue in nodes we don't care about.
*/
checkForMaxIndentation(node);
};

nodes.Literal = removeInvalidNodeErrorsInLiteral;
nodes.TemplateElement = removeInvalidNodeErrorsInTemplateLiteral;
nodes["Program:exit"] = function () {
// First strip errors occurring in comment nodes.
commentNodes.forEach(removeInvalidNodeErrorsInComment);

// If we have any errors remaining report on them
errors.forEach((error) => context.report(error));
};
} else {
nodes.Program = () => {};
}

return nodes;
},
});
4 changes: 4 additions & 0 deletions src/utils/type-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const isFunctionDeclaration = (
);
};

export const isLiteral = (node: TSESTree.Node): node is TSESTree.Literal => {
return node.type === "Literal";
};

export const isIdentifier = (
node: TSESTree.Expression | TSESTree.PrivateIdentifier
): node is TSESTree.Identifier => {
Expand Down
Loading