Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist/
sample_cases/
wasm-toolchain
coverage
reference
reference
.vscode/
155 changes: 155 additions & 0 deletions docs/rules/logger-in-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# logger-in-checker

> Ensure logger functions are called only inside their corresponding checker if statements

## Rule Details

This rule enforces that logger functions must be guarded by their corresponding checker conditions to avoid unnecessary performance costs.

**Key Problem**: Even when logging is disabled, string interpolation and serialization still execute:

```ts
// Bad: String is always constructed, even if logging is disabled
debugLogger(`User ${userId} performed action ${action}`);

// Good: String construction only happens when logging is enabled
if (isDebugEnabled()) {
debugLogger(`User ${userId} performed action ${action}`);
}
```

Without the guard, your code wastes CPU cycles building log messages that will never be used.

## Rule Options

This rule requires configuration. You must specify an array of logger-checker pairs:

```json
{
"assemblyscript/logger-in-checker": [
"error",
[
{ "loggerName": "debugLogger", "checkerName": "isDebugEnabled" },
{ "loggerName": "traceLogger", "checkerName": "isTraceEnabled" }
]
]
}
```

Each pair consists of:

- `loggerName`: The name of the logger function that must be guarded
- `checkerName`: The name of the checker function/variable that must be used in the if condition

## Examples

### Incorrect

```ts
// String serialization happens even if logging is disabled
debugLogger(`Debug message: ${expensiveOperation()}`);

// Logger called inside wrong checker
if (wrongChecker()) {
debugLogger("Debug message");
}

// Logger called outside the checker block
if (isDebugEnabled()) {
// some code
}
debugLogger(`Expensive ${serialization()}`); // String built even if disabled

// Combined conditions are not supported
if (isDebugEnabled() && otherCondition) {
debugLogger("Debug message"); // Invalid: checker is in combined condition
}

if (otherCondition || isDebugEnabled()) {
debugLogger("Debug message"); // Invalid: checker is in combined condition
}

// Comparing checker to non-literal is not supported
if (isDebugEnabled() < someCall()) {
debugLogger("Debug message"); // Invalid: both sides are function calls
}
```

### Correct

```ts
// String only built when logging is enabled
if (isDebugEnabled()) {
debugLogger(`Debug message: ${expensiveOperation()}`);
}

// Checker as boolean variable
if (isDebugEnabled) {
debugLogger("Debug message");
}

// Checker compared to boolean literal
if (isDebugEnabled() === true) {
debugLogger("Debug message");
}

if (isDebugEnabled() == false) {
debugLogger("Debug message");
}

// Checker with comparison operators
if (logLevel() > 0) {
debugLogger("Debug message");
}

if (logLevel() >= 1) {
debugLogger("Debug message");
}

if (errorCount() < 10) {
errorLogger("Error occurred");
}

if (warningCount() <= 5) {
warningLogger("Warning");
}

// Literal on left side also works
if (0 < logLevel()) {
debugLogger("Debug message");
}

// Nested if statements are fine
if (isDebugEnabled()) {
if (otherCondition) {
debugLogger("Debug message");
}
}
```

## Configuration Example

Here's a complete example of how to configure this rule in your ESLint config:

```javascript
// eslint.config.mjs
import assemblyscript from "assemblyscript-eslint-plugin";

export default [
{
plugins: {
assemblyscript,
},
rules: {
"assemblyscript/logger-in-checker": [
"error",
[
{ loggerName: "debugLog", checkerName: "DEBUG_ENABLED" },
{ loggerName: "traceLog", checkerName: "TRACE_ENABLED" },
{ loggerName: "verboseLog", checkerName: "isVerbose" },
],
],
},
},
];
```
2 changes: 2 additions & 0 deletions plugins/asPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import noConcatString from "./rules/noConcatString.js";
import noSpread from "./rules/noSpread.js";
import noUnsupportedKeyword from "./rules/noUnsupportedKeyword.js";
import specifyType from "./rules/specifyType.js";
import loggerInChecker from "./rules/loggerInChecker.js";

export default {
rules: {
Expand All @@ -18,5 +19,6 @@ export default {
"no-unsupported-keyword": noUnsupportedKeyword,
"specify-type": specifyType,
"no-concat-string": noConcatString,
"logger-in-checker": loggerInChecker,
},
};
146 changes: 146 additions & 0 deletions plugins/rules/loggerInChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
import createRule from "../utils/createRule.js";

/**
* Rule: Ensure logger functions are called only inside their corresponding checker if statements.
*/

type LoggerCheckerPair = {
loggerName: string;
checkerName: string;
};

type Options = [LoggerCheckerPair[]];

/**
* Check if an expression is the checker function or identifier
*/
function containsCheckerCall(
node: TSESTree.Expression,
checkerName: string
): boolean {
// Direct call: checkerName()
if (
node.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === checkerName
) {
return true;
}

// Just the identifier: if(checkerName)
if (node.type === AST_NODE_TYPES.Identifier && node.name === checkerName) {
return true;
}

// Binary expressions: checkerName() === true, checkerName() > 0, etc.
if (node.type === AST_NODE_TYPES.BinaryExpression) {
const leftIsLiteral = node.left.type === AST_NODE_TYPES.Literal;
const rightIsLiteral = node.right.type === AST_NODE_TYPES.Literal;

// Left is checker, right is literal
if (
rightIsLiteral &&
node.left.type !== AST_NODE_TYPES.PrivateIdentifier &&
containsCheckerCall(node.left, checkerName)
) {
return true;
}

// Right is checker, left is literal
if (leftIsLiteral && containsCheckerCall(node.right, checkerName)) {
return true;
}
}

return false;
}

/**
* Check if a node is inside an if statement with the required checker function call
*/
function isInsideCheckerIf(node: TSESTree.Node, checkerName: string): boolean {
let current: TSESTree.Node | undefined = node;

while (current) {
// Check if we're inside an if statement with the checker function
if (
current.type === AST_NODE_TYPES.IfStatement &&
containsCheckerCall(current.test, checkerName)
) {
return true;
}

current = current.parent;
}

return false;
}

export default createRule<Options, "loggerNotInChecker">({
name: "logger-in-checker",
meta: {
type: "problem",
docs: {
description:
"Ensure logger functions are called only inside their corresponding checker if statements",
},
messages: {
loggerNotInChecker:
"Logger function '{{loggerName}}' must be called inside an if({{checkerName}}) statement",
},
schema: [
{
type: "array",
items: {
type: "object",
properties: {
loggerName: {
type: "string",
},
checkerName: {
type: "string",
},
},
required: ["loggerName", "checkerName"],
additionalProperties: false,
},
},
],
},
defaultOptions: [[]],
create(context) {
const [pairs] = context.options;

if (!pairs || pairs.length === 0) {
return {};
}

// Create a map for quick lookup
const loggerToChecker = new Map<string, string>();
for (const pair of pairs) {
loggerToChecker.set(pair.loggerName, pair.checkerName);
}

return {
CallExpression(node) {
// Check if this is a call to one of the logger functions
if (node.callee.type === AST_NODE_TYPES.Identifier) {
const functionName = node.callee.name;
const requiredChecker = loggerToChecker.get(functionName);

if (requiredChecker && !isInsideCheckerIf(node, requiredChecker)) {
context.report({
node,
messageId: "loggerNotInChecker",
data: {
loggerName: functionName,
checkerName: requiredChecker,
},
});
}
}
},
};
},
});
Loading