Skip to content

Commit 94b5b7d

Browse files
feature: add logger in checker rule
1 parent 0debd21 commit 94b5b7d

5 files changed

Lines changed: 660 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ dist/
33
sample_cases/
44
wasm-toolchain
55
coverage
6-
reference
6+
reference
7+
.vscode/

docs/rules/logger-in-checker.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# logger-in-checker
2+
3+
> Ensure logger functions are called only inside their corresponding checker if statements
4+
5+
## Rule Details
6+
7+
This rule enforces that logger functions must be guarded by their corresponding checker conditions to avoid unnecessary performance costs.
8+
9+
**Key Problem**: Even when logging is disabled, string interpolation and serialization still execute:
10+
11+
```ts
12+
// Bad: String is always constructed, even if logging is disabled
13+
debugLogger(`User ${userId} performed action ${action}`);
14+
15+
// Good: String construction only happens when logging is enabled
16+
if (isDebugEnabled()) {
17+
debugLogger(`User ${userId} performed action ${action}`);
18+
}
19+
```
20+
21+
Without the guard, your code wastes CPU cycles building log messages that will never be used.
22+
23+
## Rule Options
24+
25+
This rule requires configuration. You must specify an array of logger-checker pairs:
26+
27+
```json
28+
{
29+
"assemblyscript/logger-in-checker": [
30+
"error",
31+
[
32+
{ "loggerName": "debugLogger", "checkerName": "isDebugEnabled" },
33+
{ "loggerName": "traceLogger", "checkerName": "isTraceEnabled" }
34+
]
35+
]
36+
}
37+
```
38+
39+
Each pair consists of:
40+
41+
- `loggerName`: The name of the logger function that must be guarded
42+
- `checkerName`: The name of the checker function/variable that must be used in the if condition
43+
44+
## Examples
45+
46+
### Incorrect
47+
48+
```ts
49+
// String serialization happens even if logging is disabled
50+
debugLogger(`Debug message: ${expensiveOperation()}`);
51+
52+
// Logger called inside wrong checker
53+
if (wrongChecker()) {
54+
debugLogger("Debug message");
55+
}
56+
57+
// Logger called outside the checker block
58+
if (isDebugEnabled()) {
59+
// some code
60+
}
61+
debugLogger(`Expensive ${serialization()}`); // String built even if disabled
62+
63+
// Combined conditions are not supported
64+
if (isDebugEnabled() && otherCondition) {
65+
debugLogger("Debug message"); // Invalid: checker is in combined condition
66+
}
67+
68+
if (otherCondition || isDebugEnabled()) {
69+
debugLogger("Debug message"); // Invalid: checker is in combined condition
70+
}
71+
72+
// Comparing checker to non-literal is not supported
73+
if (isDebugEnabled() < someCall()) {
74+
debugLogger("Debug message"); // Invalid: both sides are function calls
75+
}
76+
```
77+
78+
### Correct
79+
80+
```ts
81+
// String only built when logging is enabled
82+
if (isDebugEnabled()) {
83+
debugLogger(`Debug message: ${expensiveOperation()}`);
84+
}
85+
86+
// Checker as boolean variable
87+
if (isDebugEnabled) {
88+
debugLogger("Debug message");
89+
}
90+
91+
// Checker compared to boolean literal
92+
if (isDebugEnabled() === true) {
93+
debugLogger("Debug message");
94+
}
95+
96+
if (isDebugEnabled() == false) {
97+
debugLogger("Debug message");
98+
}
99+
100+
// Checker with comparison operators
101+
if (logLevel() > 0) {
102+
debugLogger("Debug message");
103+
}
104+
105+
if (logLevel() >= 1) {
106+
debugLogger("Debug message");
107+
}
108+
109+
if (errorCount() < 10) {
110+
errorLogger("Error occurred");
111+
}
112+
113+
if (warningCount() <= 5) {
114+
warningLogger("Warning");
115+
}
116+
117+
// Literal on left side also works
118+
if (0 < logLevel()) {
119+
debugLogger("Debug message");
120+
}
121+
122+
// Nested if statements are fine
123+
if (isDebugEnabled()) {
124+
if (otherCondition) {
125+
debugLogger("Debug message");
126+
}
127+
}
128+
```
129+
130+
## Configuration Example
131+
132+
Here's a complete example of how to configure this rule in your ESLint config:
133+
134+
```javascript
135+
// eslint.config.mjs
136+
import assemblyscript from "assemblyscript-eslint-plugin";
137+
138+
export default [
139+
{
140+
plugins: {
141+
assemblyscript,
142+
},
143+
rules: {
144+
"assemblyscript/logger-in-checker": [
145+
"error",
146+
[
147+
{ loggerName: "debugLog", checkerName: "DEBUG_ENABLED" },
148+
{ loggerName: "traceLog", checkerName: "TRACE_ENABLED" },
149+
{ loggerName: "verboseLog", checkerName: "isVerbose" },
150+
],
151+
],
152+
},
153+
},
154+
];
155+
```

plugins/asPlugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import noConcatString from "./rules/noConcatString.js";
1010
import noSpread from "./rules/noSpread.js";
1111
import noUnsupportedKeyword from "./rules/noUnsupportedKeyword.js";
1212
import specifyType from "./rules/specifyType.js";
13+
import loggerInChecker from "./rules/loggerInChecker.js";
1314

1415
export default {
1516
rules: {
@@ -18,5 +19,6 @@ export default {
1819
"no-unsupported-keyword": noUnsupportedKeyword,
1920
"specify-type": specifyType,
2021
"no-concat-string": noConcatString,
22+
"logger-in-checker": loggerInChecker,
2123
},
2224
};

plugins/rules/loggerInChecker.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
2+
import createRule from "../utils/createRule.js";
3+
4+
/**
5+
* Rule: Ensure logger functions are called only inside their corresponding checker if statements.
6+
*/
7+
8+
type LoggerCheckerPair = {
9+
loggerName: string;
10+
checkerName: string;
11+
};
12+
13+
type Options = [LoggerCheckerPair[]];
14+
15+
/**
16+
* Check if an expression is the checker function or identifier
17+
*/
18+
function containsCheckerCall(
19+
node: TSESTree.Expression,
20+
checkerName: string
21+
): boolean {
22+
// Direct call: checkerName()
23+
if (
24+
node.type === AST_NODE_TYPES.CallExpression &&
25+
node.callee.type === AST_NODE_TYPES.Identifier &&
26+
node.callee.name === checkerName
27+
) {
28+
return true;
29+
}
30+
31+
// Just the identifier: if(checkerName)
32+
if (node.type === AST_NODE_TYPES.Identifier && node.name === checkerName) {
33+
return true;
34+
}
35+
36+
// Binary expressions: checkerName() === true, checkerName() > 0, etc.
37+
if (node.type === AST_NODE_TYPES.BinaryExpression) {
38+
const leftIsLiteral = node.left.type === AST_NODE_TYPES.Literal;
39+
const rightIsLiteral = node.right.type === AST_NODE_TYPES.Literal;
40+
41+
// Left is checker, right is literal
42+
if (
43+
rightIsLiteral &&
44+
node.left.type !== AST_NODE_TYPES.PrivateIdentifier &&
45+
containsCheckerCall(node.left, checkerName)
46+
) {
47+
return true;
48+
}
49+
50+
// Right is checker, left is literal
51+
if (leftIsLiteral && containsCheckerCall(node.right, checkerName)) {
52+
return true;
53+
}
54+
}
55+
56+
return false;
57+
}
58+
59+
/**
60+
* Check if a node is inside an if statement with the required checker function call
61+
*/
62+
function isInsideCheckerIf(node: TSESTree.Node, checkerName: string): boolean {
63+
let current: TSESTree.Node | undefined = node;
64+
65+
while (current) {
66+
// Check if we're inside an if statement with the checker function
67+
if (
68+
current.type === AST_NODE_TYPES.IfStatement &&
69+
containsCheckerCall(current.test, checkerName)
70+
) {
71+
return true;
72+
}
73+
74+
current = current.parent;
75+
}
76+
77+
return false;
78+
}
79+
80+
export default createRule<Options, "loggerNotInChecker">({
81+
name: "logger-in-checker",
82+
meta: {
83+
type: "problem",
84+
docs: {
85+
description:
86+
"Ensure logger functions are called only inside their corresponding checker if statements",
87+
},
88+
messages: {
89+
loggerNotInChecker:
90+
"Logger function '{{loggerName}}' must be called inside an if({{checkerName}}) statement",
91+
},
92+
schema: [
93+
{
94+
type: "array",
95+
items: {
96+
type: "object",
97+
properties: {
98+
loggerName: {
99+
type: "string",
100+
},
101+
checkerName: {
102+
type: "string",
103+
},
104+
},
105+
required: ["loggerName", "checkerName"],
106+
additionalProperties: false,
107+
},
108+
},
109+
],
110+
},
111+
defaultOptions: [[]],
112+
create(context) {
113+
const [pairs] = context.options;
114+
115+
if (!pairs || pairs.length === 0) {
116+
return {};
117+
}
118+
119+
// Create a map for quick lookup
120+
const loggerToChecker = new Map<string, string>();
121+
for (const pair of pairs) {
122+
loggerToChecker.set(pair.loggerName, pair.checkerName);
123+
}
124+
125+
return {
126+
CallExpression(node) {
127+
// Check if this is a call to one of the logger functions
128+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
129+
const functionName = node.callee.name;
130+
const requiredChecker = loggerToChecker.get(functionName);
131+
132+
if (requiredChecker && !isInsideCheckerIf(node, requiredChecker)) {
133+
context.report({
134+
node,
135+
messageId: "loggerNotInChecker",
136+
data: {
137+
loggerName: functionName,
138+
checkerName: requiredChecker,
139+
},
140+
});
141+
}
142+
}
143+
},
144+
};
145+
},
146+
});

0 commit comments

Comments
 (0)