Skip to content

Commit c52c2d4

Browse files
authored
Feat: Enhance rule set (#4)
New Rules Added: no-concat-string Rule: Prohibits string concatenation inside loops to avoid performance issues caused by repeated memory allocation and garbage collection. Suggests alternatives like array.join() or string builders. [1] [2] specify-type Rule: Enforces explicit type annotations for variable declarations, especially for numeric types, to enhance WebAssembly type safety and performance. [1] [2]
1 parent 32cc730 commit c52c2d4

17 files changed

+832
-33
lines changed

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
dist/**
33
reference/**
44
sample_cases/**
5-
sample_config/**
65
build/**
76
wasm-toolchain/**
87
coverage/**

cspell/project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ typechecker
1717
tsconfig
1818
tseslint
1919
tses
20+
sonarjs
2021

2122
// AssemblyScript types and keywords
2223
i8

docs/rules/no-concat-string.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# no-concat-string
2+
3+
> Disallow string concatenation in loops
4+
5+
## Rule Details
6+
7+
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.
8+
9+
## Rule Options
10+
11+
This rule has no configuration options.
12+
13+
## Examples
14+
15+
### Incorrect
16+
17+
```ts
18+
// String concatenation with + operator in loop
19+
for (let i = 0; i < 1000; i++) {
20+
const message = "Count: " + i; // not recommended
21+
}
22+
23+
// String concatenation with += operator in loop
24+
let result = "";
25+
for (let i = 0; i < 1000; i++) {
26+
result += "Item " + i; // not recommended
27+
}
28+
29+
// String methods used in concatenation
30+
let formatted = "";
31+
for (let i = 0; i < items.length; i++) {
32+
formatted += text.toUpperCase() + " "; // not recommended
33+
}
34+
35+
// Object property string concatenation
36+
const obj = { message: "" };
37+
for (let i = 0; i < 10; i++) {
38+
obj.message += "test"; // not recommended
39+
}
40+
```
41+
42+
### Correct
43+
44+
```ts
45+
// Use array join for better performance
46+
const parts: string[] = [];
47+
for (let i = 0; i < 1000; i++) {
48+
// add items to parts
49+
// ...
50+
}
51+
const result = parts.join("");
52+
53+
// String concatenation outside loops is allowed
54+
const greeting = "Hello " + name;
55+
56+
// String operations that don't involve concatenation
57+
for (let i = 0; i < items.length; i++) {
58+
const formatted = items[i].toString();
59+
console.log(formatted);
60+
}
61+
```
62+
63+
## When Not To Use
64+
65+
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.

docs/rules/specify-type.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# specify-type
2+
3+
> Enforce explicit type annotations for variable declarations
4+
5+
## Rule Details
6+
7+
This rule enforces explicit type annotations for:
8+
9+
1. **Floating point literals** - default inferred float type f64 might not always be the optimal choice, and should be specified explicitly (f32 or f64)
10+
2. **Uninitialized variables** - to ensure type safety
11+
12+
Integer literals are allowed to use AssemblyScript's default type inference (which infers `i32`), as this is typically the desired behavior and maintains good ergonomics.
13+
14+
## Rule Options
15+
16+
This rule has no configuration options.
17+
18+
## Examples
19+
20+
### Incorrect
21+
22+
```ts
23+
// Missing type annotation for floating-point literal
24+
const mileage = 5.3; // requires explicit f32 or f64 annotation
25+
26+
// No type annotation and no initialization
27+
let count; // missing type annotation
28+
29+
// Array with floating-point literals without type annotation
30+
const scores = [75.5, 82.3, 90.1]; // requires explicit type annotation
31+
```
32+
33+
### Correct
34+
35+
```ts
36+
// Explicit type annotation for floating-point values
37+
const mileage: f32 = 5.3; // recommended - specifies precision
38+
const pi: f64 = 3.14159; // or f64 for double precision
39+
40+
// Explicit type annotation when no initialization
41+
let count: i32; // recommended
42+
43+
// Array with explicit type annotation
44+
const scores: f32[] = [75.5, 82.3, 90.1]; // recommended
45+
const numbers: i32[] = new Array<i32>(); // recommended
46+
47+
// Integer literals and const variables with non-numeric types (type can be inferred)
48+
const numbers = [1, 2, 3]; // allowed - integers default to i32
49+
const count = 42; // allowed - integer defaults to i32
50+
const message = "hello"; // allowed - string literals
51+
const isValid = true; // allowed - boolean literals
52+
```
53+
54+
## Rationale
55+
56+
- **WebAssembly Type Safety**: AssemblyScript assigns float variable default to f64, and may not always be optimal
57+
- **Numeric Type Distinction**: JavaScript numbers don't distinguish between f32 and f64, so explicit typing is crucial for performance
58+
- **Code Clarity**: Explicit types make the intended floating point precision clear to developers

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
],
3434
"scripts": {
3535
"build": "npx tsc --build ./tsconfig.json",
36-
"test": "mocha 'dist/tests/**/**.test.js'",
36+
"test": "mocha --timeout 10000 'dist/tests/**/**.test.js'",
3737
"test:coverage": "c8 npm test",
3838
"lint": "npx eslint plugins/ tests/",
3939
"watch": "npx tsc --build ./tsconfig.json --watch",

plugins/asPlugin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
* or violate best practices specific to the language.
77
*/
88
import dontOmitElse from "./rules/dontOmitElse.js";
9+
import noConcatString from "./rules/noConcatString.js";
910
import noSpread from "./rules/noSpread.js";
1011
import noUnsupportedKeyword from "./rules/noUnsupportedKeyword.js";
12+
import specifyType from "./rules/specifyType.js";
1113

1214
export default {
1315
rules: {
1416
"dont-omit-else": dontOmitElse,
1517
"no-spread": noSpread,
1618
"no-unsupported-keyword": noUnsupportedKeyword,
19+
"specify-type": specifyType,
20+
"no-concat-string": noConcatString,
1721
},
1822
};

plugins/rules/arrayInitStyle.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import createRule from "../utils/createRule.js";
55
* Rule: Array Initializer
66
* Avoid using [] to initialize variables
77
* [] will create a temporary object in data section.
8-
* BAD
9-
* let v: i32[] = [];
10-
* GOOD
11-
* let v: i32[] = new Array();
128
*/
139
const arrayInitStyle: ESLintUtils.RuleModule<
1410
"preferArrayConstructor",

plugins/rules/dontOmitElse.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import createRule from "../utils/createRule.js";
55
/**
66
* Rule: Dont Omit Else
77
* Enforce using else block when if branch doesn't contain control flow statement.
8-
* BAD
9-
* if (x) { }
10-
* GOOD
11-
* if (x) { } else { }
128
*/
139

1410
// Helper function to check if a node will lead to early exit

plugins/rules/noConcatString.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { ESLintUtils } from "@typescript-eslint/utils";
2+
import createRule from "../utils/createRule.js";
3+
import ts from "typescript";
4+
5+
/**
6+
* Rule: Don't allow string concatenation in loops as this can incur performance penalties.
7+
*/
8+
9+
export default createRule({
10+
name: "no-concat-string",
11+
meta: {
12+
type: "problem",
13+
docs: {
14+
description: "Disallow string concatenation in loops",
15+
},
16+
messages: {
17+
noConcatInLoop:
18+
"String concatenation inside loops can lead to performance issues. Use array.join() or a string builder instead.",
19+
},
20+
schema: [], // no options
21+
},
22+
defaultOptions: [],
23+
create(context) {
24+
// We uses built-in type system to deduce type information
25+
// https://typescript-eslint.io/developers/custom-rules/#typed-rules
26+
// https://typescript-eslint.io/getting-started/typed-linting/
27+
function isStringType(type: ts.Type): boolean {
28+
// basic string type
29+
if (type.flags & ts.TypeFlags.String) {
30+
return true;
31+
}
32+
33+
// String literal type (e.g., 'hello')
34+
if (type.flags & ts.TypeFlags.StringLiteral) {
35+
return true;
36+
}
37+
38+
// Template literal type (e.g., `hello${string}`)
39+
if (type.flags & ts.TypeFlags.TemplateLiteral) {
40+
return true;
41+
}
42+
43+
// String in union type (e.g., string | number)
44+
if (type.isUnion()) {
45+
return type.types.some((t) => isStringType(t));
46+
}
47+
48+
// Type alias (e.g., type MyString = string)
49+
if (type.aliasSymbol) {
50+
return isStringType(checker.getDeclaredTypeOfSymbol(type.aliasSymbol));
51+
}
52+
53+
return false;
54+
}
55+
// Track the loop nesting level
56+
let loopDepth = 0;
57+
// Grab the parser services for the rule
58+
const parserServices = ESLintUtils.getParserServices(context);
59+
// Grab the TypeScript type checker
60+
const checker = parserServices.program.getTypeChecker();
61+
62+
return {
63+
// Track entry and exit for loops
64+
// Upon entering, loopDepth will increment; upon exiting loopDepth will decrease
65+
// We use this mechanism to detect whether we are in a loop or not (when in loop, loopDepth will be a value greater than 0)
66+
ForStatement() {
67+
loopDepth++;
68+
},
69+
"ForStatement:exit"() {
70+
loopDepth--;
71+
},
72+
73+
WhileStatement() {
74+
loopDepth++;
75+
},
76+
"WhileStatement:exit"() {
77+
loopDepth--;
78+
},
79+
80+
DoWhileStatement() {
81+
loopDepth++;
82+
},
83+
"DoWhileStatement:exit"() {
84+
loopDepth--;
85+
},
86+
87+
ForInStatement() {
88+
loopDepth++;
89+
},
90+
"ForInStatement:exit"() {
91+
loopDepth--;
92+
},
93+
94+
ForOfStatement() {
95+
loopDepth++;
96+
},
97+
"ForOfStatement:exit"() {
98+
loopDepth--;
99+
},
100+
101+
// Check for string concatenation with + operator
102+
BinaryExpression(node) {
103+
// Only check inside loops
104+
if (loopDepth === 0) return;
105+
106+
const leftType: ts.Type = parserServices.getTypeAtLocation(node.left);
107+
const rightType: ts.Type = parserServices.getTypeAtLocation(node.right);
108+
109+
if (
110+
node.operator === "+" &&
111+
(isStringType(leftType) || isStringType(rightType))
112+
) {
113+
context.report({
114+
node,
115+
messageId: "noConcatInLoop",
116+
});
117+
}
118+
},
119+
120+
// Check for string concatenation with += operator
121+
AssignmentExpression(node) {
122+
if (loopDepth === 0) {
123+
return;
124+
}
125+
126+
if (node.operator === "+=") {
127+
// Check if right side is a string type
128+
const rightType: ts.Type = parserServices.getTypeAtLocation(
129+
node.right
130+
);
131+
132+
const rightIsString = isStringType(rightType);
133+
const leftType: ts.Type = parserServices.getTypeAtLocation(node.left);
134+
const leftIsString = isStringType(leftType);
135+
136+
// Check different left side patterns
137+
let shouldReport = false;
138+
139+
shouldReport = leftIsString || rightIsString;
140+
141+
if (shouldReport) {
142+
context.report({
143+
node,
144+
messageId: "noConcatInLoop",
145+
});
146+
}
147+
}
148+
},
149+
};
150+
},
151+
});

plugins/rules/noSpread.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import createRule from "../utils/createRule.js";
33

44
/**
55
* Rule: No Spread
6-
* Reject usages of ...var on call expressions, as spread syntax
7-
* is not supported in AssemblyScript.
8-
* BAD:
9-
* foo(1, ...bar)
10-
* GOOD:
11-
* foo(1, bar)
6+
* Reject usages of ...var on call expressions, as spread syntax is not supported in AssemblyScript.
127
*/
138
const noSpread: ESLintUtils.RuleModule<
149
"noSpreadMsg",

0 commit comments

Comments
 (0)