diff --git a/.gitignore b/.gitignore index a7e6d18..c0c5b24 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ dist/ sample_cases/ wasm-toolchain coverage -.vscode reference \ No newline at end of file diff --git a/Readme.md b/Readme.md index 1323887..109c25d 100644 --- a/Readme.md +++ b/Readme.md @@ -25,10 +25,22 @@ Optimizes code for better WebAssembly performance: - `array-init-style`: Recommends using `new Array()` instead of `[]` for initializing empty arrays +- `no-repeated-member-access`: Recommends extracting repeated member access to improve performance + ## Configuration See `sample_config/sample_eslint.config.mjs` for a detailed example of how to configure and use this plugin. +It includes some other pre-written rules including: + +- `no-implicit-globals`: Warns against creating implicit global variables +- `curly`: Requires curly braces for all control statements to prevent error-prone one-liner code +- `@typescript-eslint/no-restricted-types`: Enforces AssemblyScript-specific type usage: + - Use `string` instead of `String` + - Use `bool` instead of `Boolean` + - Disallows unsupported types like `undefined` and `object` +- `@typescript-eslint/adjacent-overload-signatures`: Requires overload signatures to be adjacent + ## Documentation For detailed rule documentation, see the [docs/rules](./docs/rules) directory. diff --git a/docs/rules/no-repeated-member-access.md b/docs/rules/no-repeated-member-access.md new file mode 100644 index 0000000..3d619f8 --- /dev/null +++ b/docs/rules/no-repeated-member-access.md @@ -0,0 +1,94 @@ +# no-repeated-member-access + +> Optimize repeated member access patterns by extracting variables + +## Rule Details + +This rule identifies repeated member access patterns in your code and suggests extracting them to variables for better performance and readability. In AssemblyScript, repeated property access can have performance implications (due to when they are compiled to WASM bytecode, they will induce more instructions), especially in loops or frequently called functions. + +This rule doesn't extract computed properties/array index. These can change unexpectedly and therefore should be avoid for extraction. Examples include: + +```ts +arr[0]; +arr[0][1]; +arr[0].property; +obj.arr[0].value; +data.items[0].config; +obj["prop"]; +obj[getKey()]; +``` + +The rule will also avoid to warn when functions are invoked upon properties, as this could have implications that alter the extracted value. +Examples include: + +```ts +x = a.b.c; +a.b.doSomething(); // this line will prevent a.b.c from being warned although it is used multiple times, as doSomething() could potentially change the value of a.b +y = a.b.c; +z = a.b.c; +``` + +## Examples + +### Incorrect + +```ts +// Repeated access to the same property chain (3+ times) +function processData(obj: MyObject): void { + if (obj.config.settings.enabled) { + obj.config.settings.value = 10; + console.log(obj.config.settings.name); + obj.config.settings.timestamp = Date.now(); + } +} + +// Deep property chains accessed multiple times +function renderUI(app: Application): void { + app.ui.layout.header.title.text = "New Title"; + app.ui.layout.header.title.fontSize = 16; + app.ui.layout.header.title.color = "blue"; +} +``` + +### Correct + +```ts +// Extract repeated property access to variables +function processData(obj: MyObject): void { + const settings = obj.config.settings; + if (settings.enabled) { + settings.value = 10; + console.log(settings.name); + settings.timestamp = Date.now(); + } +} + +// Extract deep property chains +function renderUI(app: Application): void { + const title = app.ui.layout.header.title; + title.text = "New Title"; + title.fontSize = 16; + title.color = "blue"; +} + +// Single or infrequent access is allowed +function singleAccess(obj: MyObject): void { + console.log(obj.config.settings.enabled); // Only accessed once +} +``` + +## Benefits + +- **Performance**: Reduces redundant property lookups, especially in tight loops +- **Readability**: Makes code more readable by giving meaningful names to complex property chains +- **Maintainability**: Easier to update property references when extracted to variables + +## When Not To Use + +- If the property chains are very short (single level) and performance is not critical +- When the object properties are frequently modified, making extraction less beneficial +- In very simple functions where the overhead of variable extraction outweighs the benefits + +## Related Rules + +- Consider using this rule alongside other performance-focused rules for optimal AssemblyScript code generation diff --git a/eslint.config.mjs b/eslint.config.mjs index 1899e6c..1cb467d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,10 +4,13 @@ import { baseConfig } from "@schleifner/eslint-config-base/config.mjs"; export default tseslint.config( { - ignores: [ - "dist/**", - "**/*.mjs", - ], + ignores: ["dist/**", "**/*.mjs"], }, - ...baseConfig + ...baseConfig, + { + files: ["**/*.ts"], + rules: { + curly: ["error", "all"] + }, + } ); diff --git a/package-lock.json b/package-lock.json index 696d08d..2e41607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "mocha": "^11.2.2", "npm-run-all": "^4.1.5", "prettier": "^3.5.3", + "ts-node": "^10.9.2", "tsx": "^4.19.3" }, "engines": { @@ -658,6 +659,19 @@ "node": ">=20" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1412,6 +1426,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", @@ -1538,6 +1563,34 @@ "typescript-eslint": "^8.29.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -2100,7 +2153,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2118,6 +2170,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2160,6 +2225,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2897,6 +2969,13 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5942,6 +6021,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7985,6 +8071,60 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -8307,6 +8447,13 @@ "punycode": "^2.1.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8720,6 +8867,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index dc762ed..e00413b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "mocha": "^11.2.2", "npm-run-all": "^4.1.5", "prettier": "^3.5.3", + "ts-node": "^10.9.2", "tsx": "^4.19.3" }, "dependencies": { diff --git a/plugins/perfPlugin.ts b/plugins/perfPlugin.ts index 119db76..f65fdc1 100644 --- a/plugins/perfPlugin.ts +++ b/plugins/perfPlugin.ts @@ -5,9 +5,11 @@ * in AssemblyScript code. */ import arrayInitStyle from "./rules/arrayInitStyle.js"; +import noRepeatedMemberAccess from "./rules/memberAccess.js"; export default { rules: { "array-init-style": arrayInitStyle, + "no-repeated-member-access": noRepeatedMemberAccess, }, }; diff --git a/plugins/rules/memberAccess.ts b/plugins/rules/memberAccess.ts new file mode 100644 index 0000000..d6a4966 --- /dev/null +++ b/plugins/rules/memberAccess.ts @@ -0,0 +1,265 @@ +import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils"; +import { Scope } from "@typescript-eslint/utils/ts-eslint"; +import createRule from "../utils/createRule.js"; + +const noRepeatedMemberAccess = createRule({ + name: "no-repeated-member-access", + meta: { + type: "suggestion", + docs: { + description: + "Optimize repeated member access patterns by extracting variables", + }, + schema: [], + fixable: "code", + messages: { + repeatedAccess: + "Member chain '{{ chain }}' is accessed multiple times. Extract to variable.", + }, + }, + defaultOptions: [], + + create(context) { + const sourceCode = context.sourceCode; + + // Track which chains have already been reported to avoid duplicate reports + const reportedChains = new Set(); + + // Tree-based approach for storing member access chains + // Each node represents a property in the chain (e.g., a -> b -> c for a.b.c) + class ChainNode { + private count: number = 0; + private modified: boolean = false; + private parent?: ChainNode; + private children: Map = new Map(); + + constructor(parent?: ChainNode) { + this.parent = parent; + this.modified = this.parent?.modified || false; + } + + get getCount(): number { + return this.count; + } + + get isModified(): boolean { + return this.modified; + } + + get getChildren(): Map { + return this.children; + } + + incrementCount(): void { + this.count++; + } + + // Get or create child node + getOrCreateChild(childName: string): ChainNode { + if (!this.children.has(childName)) { + this.children.set(childName, new ChainNode(this)); + } + return this.children.get(childName)!; + } + + // Mark this node and all its descendants as modified + markAsModified(): void { + this.modified = true; + for (const child of this.children.values()) { + child.markAsModified(); + } + } + } + + // Root node for the tree (per scope) + class ChainTree { + private root: ChainNode = new ChainNode(); + + // Visitor function to navigate through property chain + private visitChainPath( + properties: string[], + process: (node: ChainNode) => void + ): ChainNode { + let current = this.root; + + // Navigate/process node in the tree + for (const prop of properties) { + const child = current.getOrCreateChild(prop); + current = child; + process(current); + } + + return current; + } + + // Insert a chain path into the tree and increment counts + insertChain(properties: string[]): void { + this.visitChainPath(properties, (node) => { + node.incrementCount(); + }); + } + + // Mark a chain and its descendants as modified + markChainAsModified(properties: string[]): void { + const targetNode = this.visitChainPath(properties, () => {}); + + // Mark this node and all descendants as modified + targetNode.markAsModified(); + } + + // Find any valid chain that meets the minimum occurrence threshold + findValidChains() { + const validChains: Array<{ chain: string }> = []; + + const dfs = (node: ChainNode, pathArray: string[]) => { + // Only consider chains with more than one segment (has dots) + if (pathArray.length > 1 && !node.isModified && node.getCount >= 2) { + validChains.push({ + chain: pathArray.join("."), + }); + } + + // Stop traversing if this node is modified + if (node.isModified) { + return; + } + + // Recursively traverse children + for (const [childName, child] of node.getChildren) { + pathArray.push(childName); + dfs(child, pathArray); + pathArray.pop(); + } + }; + + // Start DFS from root with empty path array + dfs(this.root, []); + return validChains; + } + } + + // Stores mapping of scope to ChainTree + const scopeDataMap = new WeakMap(); + + function getChainTree(scope: Scope.Scope): ChainTree { + if (!scopeDataMap.has(scope)) { + scopeDataMap.set(scope, new ChainTree()); + } + return scopeDataMap.get(scope)!; + } + + // This function generates ["a", "b", "c"] from a.b.c (just the property names) + // The tree structure will handle the hierarchy automatically + // eslint-disable-next-line unicorn/consistent-function-scoping + function analyzeChain(node: TSESTree.MemberExpression): string[] { + const properties: string[] = []; // AST is iterated in reverse order + let current: TSESTree.Node = node; // Current node in traversal + + // Collect property chain (reverse order) + // Example: For a.b.c, we'd collect ["c", "b", "a"] initially + while (current.type === AST_NODE_TYPES.MemberExpression) { + if (current.computed) { + // skip computed properties like obj["prop"] or arr[0] or obj[getKey()] + break; + } else { + // Handle dot notation like obj.prop + properties.push(current.property.name); + } + + current = current.object; // Move to parent object + + // Handle TSNonNullExpression (the ! operator) + while (current.type === AST_NODE_TYPES.TSNonNullExpression) { + current = current.expression; + } + } + + // Handle base object (the root of the chain) + // Example: For a.b.c, the base object is "a" + if (current.type === AST_NODE_TYPES.Identifier) { + properties.push(current.name); // Add base object name + } else if (current.type === AST_NODE_TYPES.ThisExpression) { + properties.push("this"); + } // ignore other patterns + + // Reverse to get forward order: ["a", "b", "c"] + properties.reverse(); + return properties; + } + + function setModifiedFlag(chain: string[], node: TSESTree.Node) { + const scope = sourceCode.getScope(node); + const chainTree = getChainTree(scope); + chainTree.markChainAsModified(chain); + } + + function processMemberExpression(node: TSESTree.MemberExpression) { + // Skip nodes that are part of larger member expressions + // Example: In a.b.c, we process the top-level MemberExpression only, + // not the sub-expressions a.b or a + if (node.parent?.type === AST_NODE_TYPES.MemberExpression) { + return; + } + + const properties = analyzeChain(node); + if (!properties || properties.length === 0) { + return; + } + + const scope = sourceCode.getScope(node); + const chainTree = getChainTree(scope); + + // Insert the chain into the tree (this will increment counts automatically) + chainTree.insertChain(properties); + + // Find all valid chains to report + const validChains = chainTree.findValidChains(); + for (const result of validChains) { + if (!reportedChains.has(result.chain)) { + context.report({ + node: node, + messageId: "repeatedAccess", + data: { chain: result.chain }, + }); + reportedChains.add(result.chain); + } + } + } + + return { + // Track assignments that modify member chains + // Example: obj.prop.val = 5 modifies the "obj.prop.val" chain + // This prevents us from extracting chains that are modified + AssignmentExpression: (node) => { + if (node.left.type === AST_NODE_TYPES.MemberExpression) { + const properties = analyzeChain(node.left); + setModifiedFlag(properties, node); + } + }, + + // Track increment/decrement operations + // Example: obj.prop.counter++ modifies "obj.prop.counter" + UpdateExpression: (node) => { + if (node.argument.type === AST_NODE_TYPES.MemberExpression) { + const properties = analyzeChain(node.argument); + setModifiedFlag(properties, node); + } + }, + + // Track function calls that might modify their arguments + // Example: obj.methods.update() might modify the "obj.methods" chain + CallExpression: (node) => { + if (node.callee.type === AST_NODE_TYPES.MemberExpression) { + const properties = analyzeChain(node.callee); + setModifiedFlag(properties, node); + } + }, + + // Process member expressions to identify repeated patterns + // Example: Catches obj.prop.val, user.settings.theme, etc. + MemberExpression: (node) => processMemberExpression(node), + }; + }, +}); + +export default noRepeatedMemberAccess; diff --git a/plugins/rules/noConcatString.ts b/plugins/rules/noConcatString.ts index 9e030a0..ecc304f 100644 --- a/plugins/rules/noConcatString.ts +++ b/plugins/rules/noConcatString.ts @@ -101,7 +101,9 @@ export default createRule({ // Check for string concatenation with + operator BinaryExpression(node) { // Only check inside loops - if (loopDepth === 0) return; + if (loopDepth === 0) { + return; + } const leftType: ts.Type = parserServices.getTypeAtLocation(node.left); const rightType: ts.Type = parserServices.getTypeAtLocation(node.right); diff --git a/tests/perfPlugin.test.ts b/tests/perfPlugin.test.ts index becd280..143227a 100644 --- a/tests/perfPlugin.test.ts +++ b/tests/perfPlugin.test.ts @@ -7,6 +7,7 @@ import { describe } from "mocha"; // Import individual rule tests to run them as part of the test suite import "./rules/arrayInitStyle.test.js"; +import "./rules/noRepeatedMemberAccess.test.js"; describe("AssemblyScript Performance ESLint Plugin", () => { // Test suite is composed of individual rule tests imported above diff --git a/tests/rules/noRepeatedMemberAccess.test.ts b/tests/rules/noRepeatedMemberAccess.test.ts new file mode 100644 index 0000000..939e333 --- /dev/null +++ b/tests/rules/noRepeatedMemberAccess.test.ts @@ -0,0 +1,174 @@ +import { describe, it } from "mocha"; +import { createRuleTester } from "../utils/testUtils.js"; +import noRepeatedMemberAccess from "../../plugins/rules/memberAccess.js"; + +describe("Rule: no-spread", () => { + const ruleTester = createRuleTester(); + + it("validates all test cases for no-repeated-member-access rule", () => { + ruleTester.run("no-repeated-member-access", noRepeatedMemberAccess, { + valid: [ + // Basic valid case + ` + const data = ctx.data[0]; + const v1 = data.v1; + `, + + // Different scopes + ` + function test() { + const a = obj.foo.bar; + } + function test2() { + const b = obj.foo.bar; + } + `, + // ignore array access + ` + const x = data[0].value; + data[0].count=data[0].count+1; + send(data[0].id); + `, + // Dynamic property access (should be ignored) + ` + const v1 = ctx[method()].value; + const v2 = ctx[method()].value; + `, + ` + switch (reason) { + case Test.x: { + return "x" + } + case Test.y: { + return "y" + } + case Test.z: { + return "z" + } + } + `, + ` + import { Juice } from "@applejuice" + export const apple = Juice.D31 + export const banana = Juice.D32 + export const cat = Juice.D33 + `, + /** + * WARN: should NOT extract [] elements as they can get modified easily + * This is implemented by detecting brackets "[]" in chains + * Examples include: + * arr[0]; + * arr[0][1]; + * arr[0].property; + * obj.arr[0].value; + * data.items[0].config; + */ + ` + const x = data[0][1].value; + data[0][1].count++; + send(data[0][1].id); + `, + ` + const a = dataset[0][1].x + dataset[0][1].y; + dataset[0][1].update(); + const b = dataset[0][1].z * 2; + notify(dataset[0][1].timestamp); + `, + ` + const first = data.items[0].config['security'].rules[2].level; + data.items[0].config['security'].rules[2].enabled = true; + validate(data.items[0].config['security'].rules[2].level); + `, + ` + const v1 = obj[123].value; + const v2 = obj[123].value; + const v3 = obj[123].value; + `, + // shouldn't report when modified + ` + const v1 = a.b.c; + a.b = {}; + const v2 = a.b.c; + const v3 = a.b.c; + `, + ` + const v1 = a.b.c; + a.b = a.b + 1; + const v2 = a.b.c; + const v3 = a.b.c; + `, + ], + + invalid: [ + // Basic invalid case + { + code: ` + const v1 = ctx.data.v1; + const v2 = ctx.data.v2; + const v3 = ctx.data.v3; + `, + errors: [{ messageId: "repeatedAccess" }], + }, + { + code: ` + const v1 = a.b.c; + const v2 = a.b.c; + const v3 = a.b.c; + `, + errors: [ + { messageId: "repeatedAccess" }, + { messageId: "repeatedAccess" }, + ], + }, + { + code: ` + const data = a.b.c.d; + const data = a.b.c.d; + const data = a.b.c.d; + const data = a.b.c; + const data = a.b.c; + + `, + errors: [ + { messageId: "repeatedAccess" }, + { messageId: "repeatedAccess" }, + { messageId: "repeatedAccess" }, + ], + }, + { + code: ` + class User { + constructor() { + this.profile = service.user.profile + this.log = service.user.logger + this.cat = service.user.cat + } + }`, + errors: [{ messageId: "repeatedAccess" }], + }, + // Nested scope case + { + code: ` + function demo() { + console.log(obj.a.b.c); + let x = obj.a.b; + return obj.a.b.d; + } + `, + errors: [ + { messageId: "repeatedAccess" }, + { messageId: "repeatedAccess" }, + ], + }, + { + code: ` + const a = data.x + data.y; + const b = data.x * 2; + notify(data.x); + `, + errors: [{ messageId: "repeatedAccess" }], + }, + ], + }); + }); +});