From f38392c400d03a2083ecd17ee0b6429742931bbf Mon Sep 17 00:00:00 2001 From: Dayong Lee Date: Tue, 24 Sep 2024 10:35:28 +0900 Subject: [PATCH] chore(lint): add lint plugin for using `for loop` instead of `for...of` over array (#587) * Add prefer for loop * change plugin name * Revert tsconfig * Revert random comment * Revert eslint config parser options --------- Co-authored-by: Sojin Park --- eslint.config.mjs | 22 ++++++++- package.json | 1 + src/array/compact.ts | 3 +- src/array/countBy.ts | 3 +- src/array/groupBy.ts | 3 +- src/array/keyBy.ts | 3 +- src/array/maxBy.ts | 3 +- src/array/minBy.ts | 3 +- src/array/partition.ts | 3 +- src/array/takeWhile.ts | 3 +- src/array/unionBy.ts | 4 +- src/array/uniqBy.ts | 3 +- src/array/uniqWith.ts | 3 +- src/compat/function/debounce.spec.ts | 16 +++--- src/compat/function/throttle.spec.ts | 11 +++-- src/compat/math/max.ts | 3 +- src/compat/math/min.ts | 3 +- src/compat/math/random.ts | 7 +-- src/compat/object/pick.ts | 3 +- src/compat/predicate/conformsTo.ts | 4 +- src/function/debounce.ts | 24 +-------- src/object/omit.ts | 3 +- src/object/omitBy.ts | 10 ++-- src/object/pick.ts | 3 +- src/object/pickBy.ts | 10 ++-- src/string/startCase.ts | 3 +- yarn.lock | 74 ++++++++++++++++++++++++++++ 27 files changed, 165 insertions(+), 66 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7e9426b0e..1e86bdb7e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import tseslint from 'typescript-eslint'; import jsdoc from 'eslint-plugin-jsdoc'; import prettier from 'eslint-config-prettier'; import pluginVue from 'eslint-plugin-vue'; +import noForOfArrayPlugin from 'eslint-plugin-no-for-of-array'; export default [ { @@ -27,7 +28,6 @@ export default [ ...globals['shared-node-browser'], ...globals.es2015, }, - parserOptions: { ecmaFeatures: { jsx: true, @@ -42,6 +42,26 @@ export default [ prettier, jsdoc.configs['flat/recommended'], ...pluginVue.configs['flat/recommended'], + { + files: ['src/**/*.ts'], + ignores: ['**/*.spec.ts'], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + 'no-for-of-array': noForOfArrayPlugin, + }, + rules: { + 'no-for-of-array/no-for-of-array': 'error', + }, + }, { rules: { 'no-implicit-coercion': 'error', diff --git a/package.json b/package.json index bee569de8..a3e3ab36e 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^50.2.2", + "eslint-plugin-no-for-of-array": "^0.0.1", "eslint-plugin-vue": "^9.28.0", "execa": "^9.3.0", "globals": "^15.9.0", diff --git a/src/array/compact.ts b/src/array/compact.ts index 7f75d1df9..358dc7981 100644 --- a/src/array/compact.ts +++ b/src/array/compact.ts @@ -14,7 +14,8 @@ type NotFalsey = Exclude; export function compact(arr: readonly T[]): Array> { const result: Array> = []; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; if (item) { result.push(item as NotFalsey); } diff --git a/src/array/countBy.ts b/src/array/countBy.ts index 6058cdfb8..acf1a7e9c 100644 --- a/src/array/countBy.ts +++ b/src/array/countBy.ts @@ -28,7 +28,8 @@ export function countBy(arr: readonly T[], mapper: (item: T) => K): Record { const result = {} as Record; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; const key = mapper(item); result[key] = (result[key] ?? 0) + 1; diff --git a/src/array/groupBy.ts b/src/array/groupBy.ts index 389e1ae66..ef0647cf2 100644 --- a/src/array/groupBy.ts +++ b/src/array/groupBy.ts @@ -33,7 +33,8 @@ export function groupBy(arr: readonly T[], getKeyFromItem: (item: T) => K): Record { const result = Object.create(null) as Record; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; const key = getKeyFromItem(item); if (result[key] == null) { diff --git a/src/array/keyBy.ts b/src/array/keyBy.ts index beea586fc..a3d05944b 100644 --- a/src/array/keyBy.ts +++ b/src/array/keyBy.ts @@ -28,7 +28,8 @@ export function keyBy(arr: readonly T[], getKeyFromItem: (item: T) => K): Record { const result = {} as Record; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; const key = getKeyFromItem(item); result[key] = item; } diff --git a/src/array/maxBy.ts b/src/array/maxBy.ts index 3e3522a40..366aabdf2 100644 --- a/src/array/maxBy.ts +++ b/src/array/maxBy.ts @@ -64,7 +64,8 @@ export function maxBy(items: readonly T[], getValue: (element: T) => number): let maxElement = items[0]; let max = -Infinity; - for (const element of items) { + for (let i = 0; i < items.length; i++) { + const element = items[i]; const value = getValue(element); if (value > max) { max = value; diff --git a/src/array/minBy.ts b/src/array/minBy.ts index 8d82b31ae..b4367bb7f 100644 --- a/src/array/minBy.ts +++ b/src/array/minBy.ts @@ -64,7 +64,8 @@ export function minBy(items: readonly T[], getValue: (element: T) => number): let minElement = items[0]; let min = Infinity; - for (const element of items) { + for (let i = 0; i < items.length; i++) { + const element = items[i]; const value = getValue(element); if (value < min) { min = value; diff --git a/src/array/partition.ts b/src/array/partition.ts index 87dbef074..759d587bc 100644 --- a/src/array/partition.ts +++ b/src/array/partition.ts @@ -24,7 +24,8 @@ export function partition(arr: readonly T[], isInTruthy: (value: T) => boolea const truthy: T[] = []; const falsy: T[] = []; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; if (isInTruthy(item)) { truthy.push(item); } else { diff --git a/src/array/takeWhile.ts b/src/array/takeWhile.ts index c445feb2c..66e385cb9 100644 --- a/src/array/takeWhile.ts +++ b/src/array/takeWhile.ts @@ -19,7 +19,8 @@ export function takeWhile(arr: readonly T[], shouldContinueTaking: (element: T) => boolean): T[] { const result: T[] = []; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; if (!shouldContinueTaking(item)) { break; } diff --git a/src/array/unionBy.ts b/src/array/unionBy.ts index 036ab287d..f949636e1 100644 --- a/src/array/unionBy.ts +++ b/src/array/unionBy.ts @@ -23,7 +23,9 @@ export function unionBy(arr1: readonly T[], arr2: readonly T[], mapper: (item: T) => U): T[] { const map = new Map(); - for (const item of [...arr1, ...arr2]) { + const items = [...arr1, ...arr2]; + for (let i = 0; i < items.length; i++) { + const item = items[i]; const key = mapper(item); if (!map.has(key)) { diff --git a/src/array/uniqBy.ts b/src/array/uniqBy.ts index 39010c780..072ae1b92 100644 --- a/src/array/uniqBy.ts +++ b/src/array/uniqBy.ts @@ -27,7 +27,8 @@ export function uniqBy(arr: readonly T[], mapper: (item: T) => U): T[] { const map = new Map(); - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; const key = mapper(item); if (!map.has(key)) { diff --git a/src/array/uniqWith.ts b/src/array/uniqWith.ts index d33bc9e3d..71be1e677 100644 --- a/src/array/uniqWith.ts +++ b/src/array/uniqWith.ts @@ -16,7 +16,8 @@ export function uniqWith(arr: readonly T[], areItemsEqual: (item1: T, item2: T) => boolean): T[] { const result: T[] = []; - for (const item of arr) { + for (let i = 0; i < arr.length; i++) { + const item = arr[i]; const isUniq = result.every(v => !areItemsEqual(v, item)); if (isUniq) { diff --git a/src/compat/function/debounce.spec.ts b/src/compat/function/debounce.spec.ts index 97bb6e405..0e6d992ed 100644 --- a/src/compat/function/debounce.spec.ts +++ b/src/compat/function/debounce.spec.ts @@ -231,7 +231,7 @@ describe('debounce', () => { expect(callCount).toBe(2); }); - it('subsequent debounced calls return the last `func` result', async done => { + it('subsequent debounced calls return the last `func` result', async () => { const debounced = debounce(identity, 32); debounced('a'); @@ -383,7 +383,7 @@ describe('debounce', () => { expect(callCount).toBe(2); }); - it('should support `maxWait` in a tight loop', async done => { + it('should support `maxWait` in a tight loop', async () => { const limit = 1000; let withCount = 0; let withoutCount = 0; @@ -460,10 +460,12 @@ describe('debounce', () => { const object = {}; const debounced = debounce( - function (this: any, value: any) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function (this: any, _: any) { actual = [this]; + // eslint-disable-next-line prefer-rest-params Array.prototype.push.apply(actual, arguments as any); - return ++callCount != 2; + return ++callCount !== 2; }, 32, { leading: true, maxWait: 64 } @@ -505,7 +507,7 @@ describe('debounce', () => { expect(callCount).toBe(isDebounce ? 1 : 2); }); - it(`\`_.${methodName}\` should invoke \`func\` with the correct \`this\` binding`, async done => { + it(`\`_.${methodName}\` should invoke \`func\` with the correct \`this\` binding`, async () => { const actual: any[] = []; const object = { funced: func(function (this: any) { @@ -526,8 +528,10 @@ describe('debounce', () => { const expected = args.slice(); const queue: any[] = args.slice(); - var funced = func(function (this: any, _: unknown) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const funced = func(function (this: any, _: unknown) { const current = [this]; + // eslint-disable-next-line prefer-rest-params Array.prototype.push.apply(current, arguments as any); actual.push(current); diff --git a/src/compat/function/throttle.spec.ts b/src/compat/function/throttle.spec.ts index 5e5a96cc6..7c7495e87 100644 --- a/src/compat/function/throttle.spec.ts +++ b/src/compat/function/throttle.spec.ts @@ -39,9 +39,8 @@ describe('throttle', () => { expect(results2[0]).not.toStrictEqual(undefined); }); - it('should clear timeout when `func` is called', async done => { + it('should clear timeout when `func` is called', async () => { let callCount = 0; - let dateCount = 0; const throttled = throttle(() => { callCount++; @@ -81,7 +80,7 @@ describe('throttle', () => { options ); - const start = +new Date(); + const start = Number(new Date()); while (Date.now() - start < limit) { throttled(); } @@ -229,7 +228,7 @@ describe('throttle', () => { expect(callCount).toBe(isDebounce ? 1 : 2); }); - it(`\`_.${methodName}\` should invoke \`func\` with the correct \`this\` binding`, async done => { + it(`\`_.${methodName}\` should invoke \`func\` with the correct \`this\` binding`, async () => { const actual: any[] = []; const object = { funced: func(function (this: any) { @@ -250,8 +249,10 @@ describe('throttle', () => { const expected = args.slice(); const queue: any[] = args.slice(); - var funced = func(function (this: any, _: unknown) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const funced = func(function (this: any, _: unknown) { const current = [this]; + // eslint-disable-next-line prefer-rest-params Array.prototype.push.apply(current, arguments as any); actual.push(current); diff --git a/src/compat/math/max.ts b/src/compat/math/max.ts index bdd74270f..43663bf71 100644 --- a/src/compat/math/max.ts +++ b/src/compat/math/max.ts @@ -37,7 +37,8 @@ export function max(items: readonly T[] = []): T | undefined { let maxElement = items[0]; let max: any = undefined; - for (const element of items) { + for (let i = 0; i < items.length; i++) { + const element = items[i]; if (max == null || element > max) { max = element; maxElement = element; diff --git a/src/compat/math/min.ts b/src/compat/math/min.ts index a9ccfa3ec..695993dfa 100644 --- a/src/compat/math/min.ts +++ b/src/compat/math/min.ts @@ -41,7 +41,8 @@ export function min(items: readonly T[] = []): T { let minElement = items[0]; let min: any = undefined; - for (const element of items) { + for (let i = 0; i < items.length; i++) { + const element = items[i]; if (min == null || element < min) { min = element; minElement = element; diff --git a/src/compat/math/random.ts b/src/compat/math/random.ts index 6911da98c..6b44b78d7 100644 --- a/src/compat/math/random.ts +++ b/src/compat/math/random.ts @@ -68,9 +68,9 @@ export function random(minimum: number, maximum: number, floating?: boolean): nu * const result3 = random(5, 5); // If the minimum is equal to the maximum, an error is thrown. */ export function random(...args: any[]): number { - let minimum: number = 0; - let maximum: number = 1; - let floating: boolean = false; + let minimum = 0; + let maximum = 1; + let floating = false; switch (args.length) { case 1: { @@ -91,6 +91,7 @@ export function random(...args: any[]): number { maximum = args[1]; } } + // eslint-disable-next-line no-fallthrough case 3: { if (typeof args[2] === 'object' && args[2] != null && args[2][args[1]] === args[0]) { minimum = 0; diff --git a/src/compat/object/pick.ts b/src/compat/object/pick.ts index 30a21de01..bdf567d34 100644 --- a/src/compat/object/pick.ts +++ b/src/compat/object/pick.ts @@ -94,7 +94,8 @@ export function pick< const result: any = {}; - for (let keys of keysArr) { + for (let i = 0; i < keysArr.length; i++) { + let keys = keysArr[i]; switch (typeof keys) { case 'object': { if (!Array.isArray(keys)) { diff --git a/src/compat/predicate/conformsTo.ts b/src/compat/predicate/conformsTo.ts index d6933a46a..52bf2f16c 100644 --- a/src/compat/predicate/conformsTo.ts +++ b/src/compat/predicate/conformsTo.ts @@ -36,7 +36,9 @@ export function conformsTo( return Object.keys(source).length === 0; } - for (const key of Object.keys(source)) { + const keys = Object.keys(source); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; const predicate = source[key]; const value = target[key]; if ((value === undefined && !(key in target)) || !predicate(value)) { diff --git a/src/function/debounce.ts b/src/function/debounce.ts index ab23af2cc..8ed41ed8f 100644 --- a/src/function/debounce.ts +++ b/src/function/debounce.ts @@ -1,26 +1,3 @@ -interface DebounceTimer { - /** - * Checks if the timer is active. - * @returns {boolean} True if the timer is active, otherwise false. - */ - isActive: () => boolean; - - /** - * Triggers the debounce timer. - * This method resets the timer and schedules the execution of the debounced function - * after the specified delay. If the timer is already active, it clears the existing timeout - * before setting a new one. - */ - trigger: () => void; - - /** - * Cancels any pending execution of the debounced function. - * This method clears the active timer, ensuring that the function will not be called - * at the end of the debounce period. It also resets any stored context or arguments. - */ - cancel: () => void; -} - interface DebounceOptions { /** * An optional AbortSignal to cancel the debounced function. @@ -158,6 +135,7 @@ export function debounce void>( return; } + // eslint-disable-next-line @typescript-eslint/no-this-alias pendingThis = this; pendingArgs = args; diff --git a/src/object/omit.ts b/src/object/omit.ts index c21594c63..b03d0626c 100644 --- a/src/object/omit.ts +++ b/src/object/omit.ts @@ -18,7 +18,8 @@ export function omit, K extends keyof T>(obj: T, keys: readonly K[]): Omit { const result = { ...obj }; - for (const key of keys) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; delete result[key]; } diff --git a/src/object/omitBy.ts b/src/object/omitBy.ts index 0c38b4507..997c971b7 100644 --- a/src/object/omitBy.ts +++ b/src/object/omitBy.ts @@ -23,12 +23,12 @@ export function omitBy>( ): Partial { const result: Partial = {}; - for (const [key, value] of Object.entries(obj)) { - if (shouldOmit(value, key)) { - continue; + const objEntries = Object.entries(obj); + for (let i = 0; i < objEntries.length; i++) { + const [key, value] = objEntries[i]; + if (!shouldOmit(value, key)) { + (result as any)[key] = value; } - - (result as any)[key] = value; } return result; diff --git a/src/object/pick.ts b/src/object/pick.ts index f3df75697..fed7407b7 100644 --- a/src/object/pick.ts +++ b/src/object/pick.ts @@ -18,7 +18,8 @@ export function pick, K extends keyof T>(obj: T, keys: readonly K[]): Pick { const result = {} as Pick; - for (const key of keys) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; result[key] = obj[key]; } diff --git a/src/object/pickBy.ts b/src/object/pickBy.ts index 244a26c07..bf4123ae9 100644 --- a/src/object/pickBy.ts +++ b/src/object/pickBy.ts @@ -23,12 +23,12 @@ export function pickBy>( ): Partial { const result: Partial = {}; - for (const [key, value] of Object.entries(obj)) { - if (!shouldPick(value, key)) { - continue; + const objEntries = Object.entries(obj); + for (let i = 0; i < objEntries.length; i++) { + const [key, value] = objEntries[i]; + if (shouldPick(value, key)) { + (result as any)[key] = value; } - - (result as any)[key] = value; } return result; diff --git a/src/string/startCase.ts b/src/string/startCase.ts index a8af0fae4..63c7ebc13 100644 --- a/src/string/startCase.ts +++ b/src/string/startCase.ts @@ -16,7 +16,8 @@ import { getWords } from './_internal/getWords.ts'; export function startCase(str: string): string { const words = getWords(str.trim()); let result = ''; - for (const word of words) { + for (let i = 0; i < words.length; i++) { + const word = words[i]; if (result) { result += ' '; } diff --git a/yarn.lock b/yarn.lock index 28a109a64..7c94c296a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2851,6 +2851,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.6.0": + version: 8.6.0 + resolution: "@typescript-eslint/scope-manager@npm:8.6.0" + dependencies: + "@typescript-eslint/types": "npm:8.6.0" + "@typescript-eslint/visitor-keys": "npm:8.6.0" + checksum: 10c0/37092ef70171c06854ac67ebfb2255063890c1c6133654e6b15b6adb6d2ab83de4feafd1599f4d02ed71a018226fcb3a389021758ec045e1904fb1798e90b4fe + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.1.0": version: 8.1.0 resolution: "@typescript-eslint/type-utils@npm:8.1.0" @@ -2873,6 +2883,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.6.0": + version: 8.6.0 + resolution: "@typescript-eslint/types@npm:8.6.0" + checksum: 10c0/e7051d212252f7d1905b5527b211e335db4ec5bb1d3a52d73c8d2de6ddf5cbc981f2c92ca9ffcef35f7447bda635ea1ccce5f884ade7f243d14f2a254982c698 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.1.0": version: 8.1.0 resolution: "@typescript-eslint/typescript-estree@npm:8.1.0" @@ -2892,6 +2909,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.6.0": + version: 8.6.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.6.0" + dependencies: + "@typescript-eslint/types": "npm:8.6.0" + "@typescript-eslint/visitor-keys": "npm:8.6.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/33ab8c03221a797865301f09d1d198c67f8b0e3dbf0d13e41f699dc2740242303a9fcfd7b38302cef318541fdedd832fd6e8ba34a5041a57e9114fa134045385 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.1.0": version: 8.1.0 resolution: "@typescript-eslint/utils@npm:8.1.0" @@ -2906,6 +2942,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.6.0": + version: 8.6.0 + resolution: "@typescript-eslint/utils@npm:8.6.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.6.0" + "@typescript-eslint/types": "npm:8.6.0" + "@typescript-eslint/typescript-estree": "npm:8.6.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10c0/5b615106342dfdf09f5a73e2554cc0c4d979c262a9a4548eb76ec7045768e0ff0bf0316cf8a5eb5404689cd476fcd335fc84f90eb985557559e42aeee33d687e + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.1.0": version: 8.1.0 resolution: "@typescript-eslint/visitor-keys@npm:8.1.0" @@ -2916,6 +2966,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.6.0": + version: 8.6.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.6.0" + dependencies: + "@typescript-eslint/types": "npm:8.6.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/9bd5d5daee9de7e009fdd1b64b1eca685a699d1b2639373bc279c97e25e769fff56fffef708ef66a2b19bc8bb201d36daf9e7084f0e0872178bfcf9d923b41f3 + languageName: node + linkType: hard + "@vitejs/plugin-vue@npm:^5.0.5": version: 5.1.2 resolution: "@vitejs/plugin-vue@npm:5.1.2" @@ -4927,6 +4987,7 @@ __metadata: eslint: "npm:^9.9.0" eslint-config-prettier: "npm:^9.1.0" eslint-plugin-jsdoc: "npm:^50.2.2" + eslint-plugin-no-for-of-array: "npm:^0.0.1" eslint-plugin-vue: "npm:^9.28.0" execa: "npm:^9.3.0" globals: "npm:^15.9.0" @@ -5336,6 +5397,19 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-no-for-of-array@npm:^0.0.1": + version: 0.0.1 + resolution: "eslint-plugin-no-for-of-array@npm:0.0.1" + dependencies: + "@typescript-eslint/utils": "npm:^8.6.0" + peerDependencies: + "@typescript-eslint/parser": ^8.6.0 + eslint: ^9.11.0 + typescript: ^5.6.2 + checksum: 10c0/37aeb5fcd71b05a5cc3c10617febc74c7da51467f1149d2e9f1b9d322b7e5090e780f8efa79007979bb52a026b8212690af74e88b35745dbc9ab3f5f6c0f14d7 + languageName: node + linkType: hard + "eslint-plugin-vue@npm:^9.28.0": version: 9.28.0 resolution: "eslint-plugin-vue@npm:9.28.0"