Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
dist/**
reference/**
sample_cases/**
sample_config/**
build/**
wasm-toolchain/**
coverage/**
1 change: 1 addition & 0 deletions cspell/project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ typechecker
tsconfig
tseslint
tses
sonarjs

// AssemblyScript types and keywords
i8
Expand Down
65 changes: 65 additions & 0 deletions docs/rules/no-concat-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# no-concat-string

> Disallow string concatenation in loops

## Rule Details

String concatenation inside loops can lead to performance issues in AssemblyScript. Each concatenation operation creates a new string object, which can cause memory allocation overhead and garbage collection pressure in tight loops. This rule enforces using alternative approaches like array joining or string builders for better performance.

## Rule Options

This rule has no configuration options.

## Examples

### Incorrect

```ts
// String concatenation with + operator in loop
for (let i = 0; i < 1000; i++) {
const message = "Count: " + i; // not recommended
}

// String concatenation with += operator in loop
let result = "";
for (let i = 0; i < 1000; i++) {
result += "Item " + i; // not recommended
}

// String methods used in concatenation
let formatted = "";
for (let i = 0; i < items.length; i++) {
formatted += text.toUpperCase() + " "; // not recommended
}

// Object property string concatenation
const obj = { message: "" };
for (let i = 0; i < 10; i++) {
obj.message += "test"; // not recommended
}
```

### Correct

```ts
// Use array join for better performance
const parts: string[] = [];
for (let i = 0; i < 1000; i++) {
// add items to parts
// ...
}
const result = parts.join("");

// String concatenation outside loops is allowed
const greeting = "Hello " + name;

// String operations that don't involve concatenation
for (let i = 0; i < items.length; i++) {
const formatted = items[i].toString();
console.log(formatted);
}
```

## When Not To Use

If you're not concerned about performance in specific use cases or working with very small loops where the performance impact is negligible, you might choose to disable this rule. However, it's generally recommended to follow this rule for better performance.
63 changes: 63 additions & 0 deletions docs/rules/specify-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# specify-type

> Enforce explicit type annotations for variable declarations

## Rule Details

In AssemblyScript, explicit type annotations are crucial for WebAssembly compilation and performance optimization. This rule enforces that variables have explicit type annotations, especially for numeric types where JavaScript's dynamic typing doesn't distinguish between different WebAssembly numeric types (i32, f32, f64, etc.).

## Rule Options

This rule has no configuration options.

## Examples

### Incorrect

```ts
// Missing type annotation for numeric literal
const mileage = 5.3; // not recommended

// No type annotation and no initialization
let count; // not recommended

// Array with numeric literals without type annotation
const numbers = [1, 2, 3]; // not recommended

// Non-const variables without type annotation
let value = "hello"; // not recommended
Comment thread
xpirad marked this conversation as resolved.
Outdated
var index = 0; // not recommended
```

### Correct

```ts
// Explicit type annotation for numeric values
const mileage: f32 = 5.3; // recommended

// Explicit type annotation when no initialization
let count: i32; // recommended

// Array with explicit type annotation
const numbers: i32[] = [1, 2, 3]; // recommended
const numbers: i32[] = new Array<i32>(); // recommended

// Non-const variables with explicit type annotations
let value: string = "hello"; // recommended
var index: i32 = 0; // recommended

// Const variables with non-numeric types (type can be inferred)
const message = "hello"; // allowed for string literals
const isValid = true; // allowed for boolean literals
```

## Rationale

- **WebAssembly Type Safety**: AssemblyScript compiles to WebAssembly, which requires explicit typing
- **Performance**: Explicit types help the compiler generate optimized WebAssembly code
Comment thread
xpirad marked this conversation as resolved.
Outdated
- **Numeric Type Distinction**: JavaScript numbers don't distinguish between i32, f32, f64, etc.
- **Code Clarity**: Explicit types make the intended data types clear to developers

## When Not To Use

This rule is specifically designed for AssemblyScript development. If you're writing regular TypeScript/JavaScript code, you might find this rule too restrictive. However, for AssemblyScript projects, explicit typing is strongly recommended for optimal compilation results.
4 changes: 4 additions & 0 deletions plugins/asPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
* or violate best practices specific to the language.
*/
import dontOmitElse from "./rules/dontOmitElse.js";
import noConcatString from "./rules/noConcatString.js";
import noSpread from "./rules/noSpread.js";
import noUnsupportedKeyword from "./rules/noUnsupportedKeyword.js";
import specifyType from "./rules/specifyType.js";

export default {
rules: {
"dont-omit-else": dontOmitElse,
"no-spread": noSpread,
"no-unsupported-keyword": noUnsupportedKeyword,
"specify-type": specifyType,
"no-concat-string": noConcatString,
},
};
193 changes: 193 additions & 0 deletions plugins/rules/noConcatString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils";
import createRule from "../utils/createRule.js";

/**
* Rule: Don't allow string concatenation in loops as this can incur performance penalties.
* Bad:
* for (let i = 0; i < 1000; i++) {
* str += "Hello" + i; // String concatenation in loop
* }
* Good:
* for (let i = 0; i < 1000; i++) {
* arr.push("Hello" + i);
* }
* const str = arr.join("");
*/

// Helper function to determine if a node is likely a string
// this is NOT 100% inclusive
function isStringType(node: TSESTree.Node) {
// String literals
if (node.type === AST_NODE_TYPES.Literal && typeof node.value === "string") {
return true;
}

// Template literals
if (node.type === AST_NODE_TYPES.TemplateLiteral) {
return true;
}

// String() constructor
if (
node.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.Identifier &&
node.callee.name === "String"
) {
return true;
}

// String methods that return strings (e.g., str.substring())
if (
node.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.MemberExpression &&
[
"charAt",
"concat",
"normalize",
"repeat",
"replace",
"replaceAll",
"slice",
"substring",
"toLowerCase",
"toUpperCase",
"trim",
"trimEnd",
"trimStart",
].includes(
node.callee.property.type === AST_NODE_TYPES.Identifier
? node.callee.property.name
: ""
)
) {
return true;
}

// Binary expression that likely results in a string
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (
node.type === AST_NODE_TYPES.BinaryExpression &&
node.operator === "+" &&
(isStringType(node.left) || isStringType(node.right))
) {
return true;
}

return false;
}
export default createRule({
name: "no-concat-string",
meta: {
type: "problem",
docs: {
description: "Disallow string concatenation in loops",
},
messages: {
noConcatInLoop:
"String concatenation inside loops can lead to performance issues. Use array.join() or a string builder instead.",
Comment thread
xpirad marked this conversation as resolved.
},
schema: [], // no options
},
defaultOptions: [],
create(context) {
// Track the loop nesting level
let loopDepth = 0;
// Track string variables
const stringVariables = new Set();

return {
// Track entry and exit for loops
ForStatement() {
loopDepth++;
},
"ForStatement:exit"() {
loopDepth--;
},

WhileStatement() {
loopDepth++;
},
"WhileStatement:exit"() {
loopDepth--;
},

DoWhileStatement() {
loopDepth++;
},
"DoWhileStatement:exit"() {
loopDepth--;
},

ForInStatement() {
loopDepth++;
},
"ForInStatement:exit"() {
loopDepth--;
},

ForOfStatement() {
loopDepth++;
},
"ForOfStatement:exit"() {
loopDepth--;
},

// Track string variable declarations
VariableDeclarator(node) {
if (
node.init &&
node.init.type === AST_NODE_TYPES.Literal &&
typeof node.init.value === "string" &&
node.id.type === AST_NODE_TYPES.Identifier
) {
stringVariables.add(node.id.name);
}
},

// Check for string concatenation with + operator
BinaryExpression(node) {
Comment thread
xpirad marked this conversation as resolved.
// Only check inside loops
if (loopDepth === 0) return;
Comment thread
xpirad marked this conversation as resolved.

if (
node.operator === "+" &&
(isStringType(node.left) || isStringType(node.right))
) {
context.report({
node,
messageId: "noConcatInLoop",
});
}
},

// Check for string concatenation with += operator
AssignmentExpression(node) {
if (loopDepth === 0) return;

if (node.operator === "+=") {
// Check if right side is a string type
const rightIsString = isStringType(node.right);

// Check different left side patterns
let shouldReport = false;

if (node.left.type === AST_NODE_TYPES.Identifier) {
// Handle: variable += "string"
shouldReport = stringVariables.has(node.left.name) || rightIsString;
} else if (node.left.type === AST_NODE_TYPES.MemberExpression) {
// Handle: obj.prop += "string" or obj["key"] += "string"
// For member expressions, we assume if right side is string, it's likely string concatenation
shouldReport = rightIsString;
}

if (shouldReport) {
context.report({
node,
messageId: "noConcatInLoop",
});
}
}
},
};
},
});
Loading