diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cdd408e3..e1d1e9de 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -83,28 +83,3 @@ jobs: run: node scripts/ci-install-eslint ${{ matrix.eslint }} - name: Test run: npm run -s test:debug - - test-cov: - name: Test and Send Coverage - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Checkout submodules - run: git submodule update --init - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - name: Install Packages - run: npm install - - name: Install ESLint v9 - run: node scripts/ci-install-eslint 9 - - name: Build - run: npm run -s build - - name: Test - run: npm run -s test:cover - - name: Send Coverage - run: npm run -s codecov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index f3273b1e..57e0668f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![npm version](https://img.shields.io/npm/v/vue-eslint-parser.svg)](https://www.npmjs.com/package/vue-eslint-parser) [![Downloads/month](https://img.shields.io/npm/dm/vue-eslint-parser.svg)](http://www.npmtrends.com/vue-eslint-parser) [![Build Status](https://github.com/vuejs/vue-eslint-parser/workflows/CI/badge.svg)](https://github.com/vuejs/vue-eslint-parser/actions) -[![Coverage Status](https://codecov.io/gh/vuejs/vue-eslint-parser/branch/master/graph/badge.svg)](https://codecov.io/gh/vuejs/vue-eslint-parser) The ESLint custom parser for `.vue` files. diff --git a/package.json b/package.json index 7b01dd7a..1012dfa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-eslint-parser", - "version": "10.1.2", + "version": "10.1.4", "description": "The ESLint custom parser for `.vue` files.", "main": "index.js", "files": [ @@ -18,7 +18,6 @@ "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", - "lodash": "^4.17.21", "semver": "^7.6.3" }, "devDependencies": { @@ -31,14 +30,12 @@ "@eslint/js": "^9.19.0", "@types/debug": "^4.1.7", "@types/estree": "^1.0.0", - "@types/lodash": "^4.14.186", "@types/mocha": "^9.0.0", "@types/node": "^18.8.4", "@types/semver": "^7.3.12", "@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/parser": "^8.22.0", "chokidar": "^3.5.2", - "codecov": "^3.8.3", "cross-spawn": "^7.0.3", "dts-bundle": "^0.7.3", "eslint": "^9.19.0", @@ -69,7 +66,6 @@ "prebuild": "npm run -s clean", "build": "tsc --module es2015 && rollup -c -o index.js && dts-bundle --name vue-eslint-parser --main .temp/index.d.ts --out ../index.d.ts", "clean": "rimraf .nyc_output .temp coverage index.*", - "codecov": "codecov", "coverage": "opener ./coverage/lcov-report/index.html", "lint": "eslint src test package.json", "pretest": "run-s build lint", @@ -86,7 +82,7 @@ "watch:tsc": "tsc --module es2015 --watch", "watch:rollup": "wait-on .temp/index.js && rollup -c -o index.js --watch", "watch:test": "wait-on index.js && warun index.js \"test/*.js\" \"test/fixtures/ast/*/*.json\" \"test/fixtures/*\" --debounce 1000 --no-initial -- nyc mocha \"test/*.js\" --reporter dot --timeout 10000", - "watch:update-ast": "wait-on index.js && warun index.js \"test/fixtures/ast/*/*.vue\" -- node scripts/update-fixtures-ast.js", + "watch:update-ast": "wait-on index.js && warun index.js \"test/fixtures/ast/*/*.vue\" -- ts-node scripts/update-fixtures-ast.js", "watch:coverage-report": "wait-on coverage/lcov-report/index.html && opener coverage/lcov-report/index.html" }, "repository": { diff --git a/rollup.config.js b/rollup.config.js index fef6483f..ce75d20c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,7 +9,7 @@ import replace from "rollup-plugin-replace" const pkg = require("./package.json") const deps = new Set( - ["assert", "events", "path"].concat(Object.keys(pkg.dependencies)) + ["assert", "events", "path"].concat(Object.keys(pkg.dependencies)), ) export default { @@ -31,5 +31,5 @@ export default { "process.env.PACKAGE_VERSION": `"${pkg.version}"`, }), ], - external: id => deps.has(id) || id.startsWith("lodash"), + external: (id) => deps.has(id), } diff --git a/src/ast/nodes.ts b/src/ast/nodes.ts index 5ebf7039..d652e4e0 100644 --- a/src/ast/nodes.ts +++ b/src/ast/nodes.ts @@ -7,7 +7,6 @@ import type { ScopeManager } from "eslint-scope" import type { ParseError } from "./errors" import type { HasLocation } from "./locations" import type { Token } from "./tokens" -// eslint-disable-next-line node/no-extraneous-import -- ignore import type { TSESTree } from "@typescript-eslint/utils" //------------------------------------------------------------------------------ diff --git a/src/common/error-utils.ts b/src/common/error-utils.ts index 378aacba..42f3f14e 100644 --- a/src/common/error-utils.ts +++ b/src/common/error-utils.ts @@ -1,5 +1,5 @@ import type { ParseError, VDocumentFragment } from "../ast/index" -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../utils/utils" /** * Insert the given error. * @param document The document that the node is belonging to. diff --git a/src/common/lines-and-columns.ts b/src/common/lines-and-columns.ts index c62d781a..b1b51085 100644 --- a/src/common/lines-and-columns.ts +++ b/src/common/lines-and-columns.ts @@ -1,4 +1,4 @@ -import sortedLastIndex from "lodash/sortedLastIndex" +import { sortedLastIndex } from "../utils/utils" import type { Location } from "../ast/index" import type { LocationCalculator } from "./location-calculator" /** diff --git a/src/common/location-calculator.ts b/src/common/location-calculator.ts index 8c8cf344..75126d07 100644 --- a/src/common/location-calculator.ts +++ b/src/common/location-calculator.ts @@ -3,7 +3,7 @@ * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ -import sortedLastIndex from "lodash/sortedLastIndex" +import { sortedLastIndex } from "../utils/utils" import type { Location } from "../ast/index" import { LinesAndColumns } from "./lines-and-columns" diff --git a/src/common/token-utils.ts b/src/common/token-utils.ts index 3013061d..21414a85 100644 --- a/src/common/token-utils.ts +++ b/src/common/token-utils.ts @@ -1,5 +1,4 @@ -import sortedIndexBy from "lodash/sortedIndexBy" -import sortedLastIndexBy from "lodash/sortedLastIndexBy" +import { sortedIndexBy, sortedLastIndexBy } from "../utils/utils" import type { LocationRange, Token, VDocumentFragment } from "../ast/index" import type { LinesAndColumns } from "./lines-and-columns" diff --git a/src/external/node-event-generator.ts b/src/external/node-event-generator.ts index b8eb78dc..a601f125 100644 --- a/src/external/node-event-generator.ts +++ b/src/external/node-event-generator.ts @@ -4,9 +4,8 @@ import type EventEmitter from "events" import type { ESQueryOptions, Selector } from "esquery" import esquery from "esquery" -import union from "lodash/union" -import intersection from "lodash/intersection" -import memoize from "lodash/memoize" +import { memoize } from "../utils/memoize" +import { union, intersection } from "../utils/utils" import type { Node } from "../ast/index" interface NodeSelector { diff --git a/src/external/token-store/utils.ts b/src/external/token-store/utils.ts index 64cc8690..574c9348 100644 --- a/src/external/token-store/utils.ts +++ b/src/external/token-store/utils.ts @@ -2,7 +2,7 @@ * @fileoverview Define utilify functions for token store. * @author Toru Nagashima */ -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../../utils/utils" import type { HasLocation } from "../../ast/index" /** diff --git a/src/html/intermediate-tokenizer.ts b/src/html/intermediate-tokenizer.ts index 62d4ca90..fbe5f546 100644 --- a/src/html/intermediate-tokenizer.ts +++ b/src/html/intermediate-tokenizer.ts @@ -4,7 +4,6 @@ * See LICENSE file in root directory for full license. */ import assert from "assert" -import last from "lodash/last" import type { ErrorCode, HasLocation, @@ -175,7 +174,7 @@ export class IntermediateTokenizer { // VExpressionEnd was not found. // Concatenate the deferred tokens to the committed token. const start = this.expressionStartToken - const end = last(this.expressionTokens) || start + const end = this.expressionTokens.at(-1) || start const value = this.expressionTokens.reduce(concat, start.value) this.expressionStartToken = null this.expressionTokens = [] @@ -240,7 +239,7 @@ export class IntermediateTokenizer { if (this.expressionStartToken != null) { // Defer this token until a VExpressionEnd token or a non-text token appear. const lastToken = - last(this.expressionTokens) || this.expressionStartToken + this.expressionTokens.at(-1) || this.expressionStartToken if (lastToken.range[1] === token.range[0]) { this.expressionTokens.push(token) return null @@ -552,7 +551,7 @@ export class IntermediateTokenizer { } const start = this.expressionStartToken - const end = last(this.expressionTokens) || start + const end = this.expressionTokens.at(-1) || start // If it's '{{}}', it's handled as a text. if (token.range[0] === start.range[1]) { diff --git a/src/html/parser.ts b/src/html/parser.ts index ed155e9a..908056c6 100644 --- a/src/html/parser.ts +++ b/src/html/parser.ts @@ -4,8 +4,6 @@ * See LICENSE file in root directory for full license. */ import assert from "assert" -import last from "lodash/last" -import findLastIndex from "lodash/findLastIndex" import type { ErrorCode, HasLocation, @@ -52,8 +50,7 @@ import { getScriptParser, getParserLangFromSFC, } from "../common/parser-options" -import sortedIndexBy from "lodash/sortedIndexBy" -import sortedLastIndexBy from "lodash/sortedLastIndexBy" +import { sortedIndexBy, sortedLastIndexBy } from "../utils/utils" import type { CustomTemplateTokenizer, CustomTemplateTokenizerConstructor, @@ -160,7 +157,7 @@ function adjustAttributeName(name: string, namespace: Namespace): string { */ function propagateEndLocation(node: VDocumentFragment | VElement): void { const lastChild = - (node.type === "VElement" ? node.endTag : null) || last(node.children) + (node.type === "VElement" ? node.endTag : null) || node.children.at(-1) if (lastChild != null) { node.range[1] = lastChild.range[1] node.loc.end = lastChild.loc.end @@ -236,7 +233,7 @@ export class Parser { * Get the current node. */ private get currentNode(): VDocumentFragment | VElement { - return last(this.elementStack) || this.document + return this.elementStack.at(-1) || this.document } /** @@ -701,8 +698,7 @@ export class Parser { protected EndTag(token: EndTag): void { debug("[html] EndTag %j", token) - const i = findLastIndex( - this.elementStack, + const i = this.elementStack.findLastIndex( (el) => el.name.toLowerCase() === token.name, ) if (i === -1) { diff --git a/src/html/tokenizer.ts b/src/html/tokenizer.ts index da071715..d18081db 100644 --- a/src/html/tokenizer.ts +++ b/src/html/tokenizer.ts @@ -1892,9 +1892,9 @@ export class Tokenizer { const type = isWhitespace(cp) ? "HTMLWhitespace" : state === "RCDATA" - ? "HTMLRawText" + ? "HTMLRCDataText" : state === "RAWTEXT" - ? "HTMLRCDataText" + ? "HTMLRawText" : "HTMLText" if (this.currentToken != null && this.currentToken.type !== type) { this.endToken() diff --git a/src/script-setup/index.ts b/src/script-setup/index.ts index 671a2bc5..81bb75bb 100644 --- a/src/script-setup/index.ts +++ b/src/script-setup/index.ts @@ -308,6 +308,10 @@ export function parseScriptSetupElements( } result.ast.tokens.sort((a, b) => a.range[0] - b.range[0]) } + + if (result.ast.comments != null) { + result.ast.comments.sort((a, b) => a.range[0] - b.range[0]) + } result.ast.body.sort((a, b) => a.range[0] - b.range[0]) const programStartOffset = result.ast.body.reduce( diff --git a/src/script/index.ts b/src/script/index.ts index 18f67d13..279095bf 100644 --- a/src/script/index.ts +++ b/src/script/index.ts @@ -3,9 +3,7 @@ * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ -import first from "lodash/first" -import last from "lodash/last" -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../utils/utils" import type { ESLintArrayExpression, ESLintArrayPattern, @@ -521,7 +519,7 @@ function parseFilter( } } - const token = last(ast.tokens)! + const token = ast.tokens!.at(-1)! return throwUnexpectedTokenError(token.value, token) } @@ -536,7 +534,7 @@ function parseFilter( // Update range. const firstToken = tokens[0] - const lastToken = last(tokens)! + const lastToken = tokens.at(-1)! expression.range = [firstToken.range[0], lastToken.range[1]] expression.loc = { start: firstToken.loc.start, end: lastToken.loc.end } @@ -778,7 +776,7 @@ export function parseExpression( } // Update range. - const lastToken = last(ret.tokens)! + const lastToken = ret.tokens.at(-1)! ret.expression.range[1] = lastToken.range[1] ret.expression.loc.end = lastToken.loc.end @@ -933,7 +931,7 @@ function parseVForExpressionForEcmaVersion5( if (open != null) { open.value = "(" } - const close = last(parsedAliases.tokens) + const close = parsedAliases.tokens.at(-1) if (close != null) { close.value = ")" } @@ -977,7 +975,7 @@ function parseVForExpressionForEcmaVersion5( comments.push(...parsedIterator.comments) const { right, references } = parsedIterator const firstToken = tokens[0] - const lastToken = last(tokens) || firstToken + const lastToken = tokens.at(-1) || firstToken const expression: VForExpression = { type: "VForExpression", range: [firstToken.range[0], lastToken.range[1]], @@ -1136,8 +1134,8 @@ function parseVOnExpressionBody( ).argument as ESLintFunctionExpression const block = functionDecl.body const body = block.body - const firstStatement = first(body) - const lastStatement = last(body) + const firstStatement = body[0] + const lastStatement = body.at(-1) const expression: VOnExpression = { type: "VOnExpression", range: [ @@ -1231,8 +1229,8 @@ export function parseSlotScopeExpression( ) const references = scope.references const variables = scope.variables - const firstParam = first(params)! - const lastParam = last(params)! + const firstParam = params[0] + const lastParam = params.at(-1)! const expression: VSlotScopeExpression = { type: "VSlotScopeExpression", range: [firstParam.range[0], lastParam.range[1]], @@ -1330,8 +1328,8 @@ export function parseGenericExpression( ) const references = scope.references const variables = scope.variables - const firstParam = first(params)! - const lastParam = last(params)! + const firstParam = params[0] + const lastParam = params.at(-1)! const expression: VGenericExpression = { type: "VGenericExpression", range: [firstParam.range[0], lastParam.range[1]], diff --git a/src/utils/memoize.ts b/src/utils/memoize.ts new file mode 100644 index 00000000..1dda47a9 --- /dev/null +++ b/src/utils/memoize.ts @@ -0,0 +1,157 @@ +/** + * Creates a memoized version of the provided function. The memoized function caches + * results based on the argument it receives, so if the same argument is passed again, + * it returns the cached result instead of recomputing it. + * + * This function works with functions that take zero or just one argument. If your function + * originally takes multiple arguments, you should refactor it to take a single object or array + * that combines those arguments. + * + * If the argument is not primitive (e.g., arrays or objects), provide a + * `getCacheKey` function to generate a unique cache key for proper caching. + * + * @template F - The type of the function to be memoized. + * @param {F} fn - The function to be memoized. It should accept a single argument and return a value. + * @param {MemoizeOptions[0], ReturnType>} [options={}] - Optional configuration for the memoization. + * @param {MemoizeCache} [options.cache] - The cache object used to store results. Defaults to a new `Map`. + * @param {(args: A) => unknown} [options.getCacheKey] - An optional function to generate a unique cache key for each argument. + * + * @returns The memoized function with an additional `cache` property that exposes the internal cache. + * + * @example + * // Example using the default cache + * const add = (x: number) => x + 10; + * const memoizedAdd = memoize(add); + * + * console.log(memoizedAdd(5)); // 15 + * console.log(memoizedAdd(5)); // 15 (cached result) + * console.log(memoizedAdd.cache.size); // 1 + * + * @example + * // Example using a custom resolver + * const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0); + * const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') }); + * console.log(memoizedSum([1, 2])); // 3 + * console.log(memoizedSum([1, 2])); // 3 (cached result) + * console.log(memoizedSum.cache.size); // 1 + * + * @example + * // Example using a custom cache implementation + * class CustomCache implements MemoizeCache { + * private cache = new Map(); + * + * set(key: K, value: T): void { + * this.cache.set(key, value); + * } + * + * get(key: K): T | undefined { + * return this.cache.get(key); + * } + * + * has(key: K): boolean { + * return this.cache.has(key); + * } + * + * delete(key: K): boolean { + * return this.cache.delete(key); + * } + * + * clear(): void { + * this.cache.clear(); + * } + * + * get size(): number { + * return this.cache.size; + * } + * } + * const customCache = new CustomCache(); + * const memoizedSumWithCustomCache = memoize(sum, { cache: customCache }); + * console.log(memoizedSumWithCustomCache([1, 2])); // 3 + * console.log(memoizedSumWithCustomCache([1, 2])); // 3 (cached result) + * console.log(memoizedAddWithCustomCache.cache.size); // 1 + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/function/memoize.ts + */ +export function memoize any>( + fn: F, + options: { + cache?: MemoizeCache> + getCacheKey?: (args: Parameters[0]) => unknown + } = {}, +): F & { cache: MemoizeCache> } { + const { cache = new Map>(), getCacheKey } = options + + const memoizedFn = function ( + this: unknown, + arg: Parameters[0], + ): ReturnType { + const key = getCacheKey ? getCacheKey(arg) : arg + + if (cache.has(key)) { + return cache.get(key)! + } + + const result = fn.call(this, arg) + + cache.set(key, result) + + return result + } + + memoizedFn.cache = cache + + return memoizedFn as F & { cache: MemoizeCache> } +} + +/** + * Represents a cache for memoization, allowing storage and retrieval of computed values. + * + * @template K - The type of keys used to store values in the cache. + * @template V - The type of values stored in the cache. + */ +interface MemoizeCache { + /** + * Stores a value in the cache with the specified key. + * + * @param key - The key to associate with the value. + * @param value - The value to store in the cache. + */ + set(key: K, value: V): void + + /** + * Retrieves a value from the cache by its key. + * + * @param key - The key of the value to retrieve. + * @returns The value associated with the key, or undefined if the key does not exist. + */ + get(key: K): V | undefined + + /** + * Checks if a value exists in the cache for the specified key. + * + * @param key - The key to check for existence in the cache. + * @returns True if the cache contains the key, false otherwise. + */ + has(key: K): boolean + + /** + * Deletes a value from the cache by its key. + * + * @param key - The key of the value to delete. + * @returns True if the value was successfully deleted, false otherwise. + */ + delete(key: K): boolean | void + + /** + * Clears all values from the cache. + */ + clear(): void + + /** + * The number of entries in the cache. + */ + size: number +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1ecc2415..87ade949 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -4,3 +4,277 @@ export function camelize(str: string) { return str.replace(/-(\w)/gu, (_, c) => (c ? c.toUpperCase() : "")) } + +/** + * A binary search implementation that finds the index at which `predicate` + * stops returning `true` and starts returning `false` (consistently) when run + * on the items of the array. It **assumes** that mapping the array via the + * predicate results in the shape `[...true[], ...false[]]`. *For any other case + * the result is unpredictable*. + * + * This is the base implementation of the `sortedIndex` functions which define + * the predicate for the user, for common use-cases. + * + * It is similar to `findIndex`, but runs at O(logN), whereas the latter is + * general purpose function which runs on any array and predicate, but runs at + * O(N) time. + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/internal/binarySearchCutoffIndex.ts#L15 + */ +function binarySearchCutoffIndex( + array: readonly T[], + predicate: (value: T, index: number, data: readonly T[]) => boolean, +): number { + let lowIndex = 0 + let highIndex = array.length + + while (lowIndex < highIndex) { + const pivotIndex = (lowIndex + highIndex) >>> 1 + const pivot = array[pivotIndex] + + if (predicate(pivot, pivotIndex, array)) { + lowIndex = pivotIndex + 1 + } else { + highIndex = pivotIndex + } + } + + return highIndex +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order; so that `splice(sortedIndex, 0, item)` would result in + * maintaining the array's sort-ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *last* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * @param item - The item to insert. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedLastIndex(item)(data) + * @example + * R.pipe(['a','a','b','c','c'], sortedLastIndex('c')) // => 5 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedLastIndex.ts#L51 + */ +export function sortedLastIndex(array: readonly T[], item: T): number { + return binarySearchCutoffIndex(array, (pivot) => pivot <= item) +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order using a value function; so that + * `splice(sortedIndex, 0, item)` would result in maintaining the arrays sort- + * ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *first* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * See also: + * * `findIndex` - scans a possibly unsorted array in-order (linear search). + * * `sortedIndex` - like this function, but doesn't take a callbackfn. + * * `sortedLastIndexBy` - like this function, but finds the last suitable index. + * * `sortedLastIndex` - like `sortedIndex`, but finds the last suitable index. + * * `rankBy` - scans a possibly unsorted array in-order, returning the index based on a sorting criteria. + * + * @param data - The (ascending) sorted array. + * @param item - The item to insert. + * @param valueFunction - All comparisons would be performed on the result of + * calling this function on each compared item. Preferably this function should + * return a `number` or `string`. This function should be the same as the one + * provided to sortBy to sort the array. The function is called exactly once on + * each items that is compared against in the array, and once at the beginning + * on `item`. When called on `item` the `index` argument is `undefined`. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedIndexBy(data, item, valueFunction) + * @example + * R.sortedIndexBy([{age:20},{age:22}],{age:21},prop('age')) // => 1 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedIndexBy.ts#L37 + */ +export function sortedIndexBy( + array: readonly T[], + item: T, + valueFunction: ( + item: T, + index: number | undefined, + data: readonly T[], + ) => number, +): number { + const value = valueFunction(item, undefined, array) + + return binarySearchCutoffIndex( + array, + (pivot, index) => valueFunction(pivot, index, array) < value, + ) +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order using a value function; so that + * `splice(sortedIndex, 0, item)` would result in maintaining the arrays sort- + * ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *last* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * See also: + * * `findIndex` - scans a possibly unsorted array in-order (linear search). + * * `sortedLastIndex` - a simplified version of this function, without a callbackfn. + * * `sortedIndexBy` - like this function, but returns the first suitable index. + * * `sortedIndex` - like `sortedLastIndex` but without a callbackfn. + * * `rankBy` - scans a possibly unsorted array in-order, returning the index based on a sorting criteria. + * + * @param data - The (ascending) sorted array. + * @param item - The item to insert. + * @param valueFunction - All comparisons would be performed on the result of + * calling this function on each compared item. Preferably this function should + * return a `number` or `string`. This function should be the same as the one + * provided to sortBy to sort the array. The function is called exactly once on + * each items that is compared against in the array, and once at the beginning + * on `item`. When called on `item` the `index` argument is `undefined`. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedLastIndexBy(data, item, valueFunction) + * @example + * R.sortedLastIndexBy([{age:20},{age:22}],{age:21},prop('age')) // => 1 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedLastIndexBy.ts#L37 + */ +export function sortedLastIndexBy( + array: readonly T[], + item: T, + valueFunction: ( + item: T, + index: number | undefined, + data: readonly T[], + ) => number, +): number { + const value = valueFunction(item, undefined, array) + + return binarySearchCutoffIndex( + array, + (pivot, index) => valueFunction(pivot, index, array) <= value, + ) +} + +/** + * Creates a duplicate-free version of an array. + * + * This function takes an array and returns a new array containing only the unique values + * from the original array, preserving the order of first occurrence. + * + * @template T - The type of elements in the array. + * @param {T[]} arr - The array to process. + * @returns {T[]} A new array with only unique values from the original array. + * + * @example + * const array = [1, 2, 2, 3, 4, 4, 5]; + * const result = uniq(array); + * // result will be [1, 2, 3, 4, 5] + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/array/uniq.ts#L16 + */ +export function uniq(arr: readonly T[]): T[] { + return Array.from(new Set(arr)) +} + +/** + * Returns the intersection of multiple arrays. + * + * This function takes multiple arrays and returns a new array containing the elements that are + * present in all provided arrays. It effectively filters out any elements that are not found + * in every array. + * + * @template T - The type of elements in the arrays. + * @param {...(ArrayLike | null | undefined)} arrays - The arrays to compare. + * @returns {T[]} A new array containing the elements that are present in all arrays. + * + * @example + * const array1 = [1, 2, 3, 4, 5]; + * const array2 = [3, 4, 5, 6, 7]; + * const result = intersection(array1, array2); + * // result will be [3, 4, 5] since these elements are in both arrays. + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/intersection.ts#L22 + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/array/intersection.ts#L19 + */ +export function intersection(...arrays: (T[] | null | undefined)[]): T[] { + if (arrays.length === 0) { + return [] + } + + let result: T[] = uniq(arrays[0]!) + + for (let i = 1; i < arrays.length; i++) { + const array = arrays[i] + const secondSet = new Set(array) + + result = result.filter((item) => secondSet.has(item)) + } + + return result +} + +/** + * This function takes multiple arrays and returns a new array containing only the unique values + * from all input arrays, preserving the order of their first occurrence. + * + * @template T - The type of elements in the arrays. + * @param {Array | null | undefined>} arrays - The arrays to inspect. + * @returns {T[]} Returns the new array of combined unique values. + * + * @example + * // Returns [2, 1] + * union([2], [1, 2]); + * + * @example + * // Returns [2, 1, 3] + * union([2], [1, 2], [2, 3]); + * + * @example + * // Returns [1, 3, 2, [5], [4]] (does not deeply flatten nested arrays) + * union([1, 3, 2], [1, [5]], [2, [4]]); + * + * @example + * // Returns [0, 2, 1] (ignores non-array values like 3 and { '0': 1 }) + * union([0], 3, { '0': 1 }, null, [2, 1]); + * @example + * // Returns [0, 'a', 2, 1] (treats array-like object { 0: 'a', length: 1 } as a valid array) + * union([0], { 0: 'a', length: 1 }, [2, 1]); + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/union.ts#L61 + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/flattenDepth.ts#L21 + */ +export function union(...arrays: T[][]): T[] { + const flattened = arrays.flat() + + return uniq(flattened) +} diff --git a/test/ast.js b/test/ast.js index c033c807..0331042e 100644 --- a/test/ast.js +++ b/test/ast.js @@ -12,7 +12,6 @@ const assert = require("assert") const fs = require("fs") const path = require("path") -const lodash = require("lodash") const parser = require("../src") const eslint = require("eslint") const semver = require("semver") @@ -116,7 +115,7 @@ function validateParent(source, parserOptions) { ruleContext.sourceCode.parserServices.defineTemplateBodyVisitor({ "*"(node) { if (stack.length >= 1) { - const parent = lodash.last(stack) + const parent = stack.at(-1) assert( node.parent === parent, `The parent of ${nodeToString( diff --git a/test/fixtures/ast/mustache-errors-in-textarea/ast.json b/test/fixtures/ast/mustache-errors-in-textarea/ast.json new file mode 100644 index 00000000..9dd73fed --- /dev/null +++ b/test/fixtures/ast/mustache-errors-in-textarea/ast.json @@ -0,0 +1,495 @@ +{ + "type": "Program", + "start": 0, + "end": 0, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 0 + } + }, + "range": [ + 0, + 0 + ], + "body": [], + "sourceType": "module", + "comments": [], + "tokens": [], + "templateBody": { + "type": "VElement", + "range": [ + 0, + 59 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 11 + } + }, + "name": "template", + "rawName": "template", + "namespace": "http://www.w3.org/1999/xhtml", + "startTag": { + "type": "VStartTag", + "range": [ + 0, + 10 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 10 + } + }, + "selfClosing": false, + "attributes": [] + }, + "children": [ + { + "type": "VText", + "range": [ + 10, + 15 + ], + "loc": { + "start": { + "line": 1, + "column": 10 + }, + "end": { + "line": 2, + "column": 4 + } + }, + "value": "\n " + }, + { + "type": "VElement", + "range": [ + 15, + 47 + ], + "loc": { + "start": { + "line": 2, + "column": 4 + }, + "end": { + "line": 2, + "column": 36 + } + }, + "name": "textarea", + "rawName": "textarea", + "namespace": "http://www.w3.org/1999/xhtml", + "startTag": { + "type": "VStartTag", + "range": [ + 15, + 25 + ], + "loc": { + "start": { + "line": 2, + "column": 4 + }, + "end": { + "line": 2, + "column": 14 + } + }, + "selfClosing": false, + "attributes": [] + }, + "children": [ + { + "type": "VExpressionContainer", + "range": [ + 25, + 36 + ], + "loc": { + "start": { + "line": 2, + "column": 14 + }, + "end": { + "line": 2, + "column": 25 + } + }, + "expression": null, + "references": [] + } + ], + "endTag": { + "type": "VEndTag", + "range": [ + 36, + 47 + ], + "loc": { + "start": { + "line": 2, + "column": 25 + }, + "end": { + "line": 2, + "column": 36 + } + } + }, + "variables": [] + }, + { + "type": "VText", + "range": [ + 47, + 48 + ], + "loc": { + "start": { + "line": 2, + "column": 36 + }, + "end": { + "line": 3, + "column": 0 + } + }, + "value": "\n" + } + ], + "endTag": { + "type": "VEndTag", + "range": [ + 48, + 59 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 11 + } + } + }, + "variables": [], + "tokens": [ + { + "type": "HTMLTagOpen", + "range": [ + 0, + 9 + ], + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "value": "template" + }, + { + "type": "HTMLTagClose", + "range": [ + 9, + 10 + ], + "loc": { + "start": { + "line": 1, + "column": 9 + }, + "end": { + "line": 1, + "column": 10 + } + }, + "value": "" + }, + { + "type": "HTMLWhitespace", + "range": [ + 10, + 15 + ], + "loc": { + "start": { + "line": 1, + "column": 10 + }, + "end": { + "line": 2, + "column": 4 + } + }, + "value": "\n " + }, + { + "type": "HTMLTagOpen", + "range": [ + 15, + 24 + ], + "loc": { + "start": { + "line": 2, + "column": 4 + }, + "end": { + "line": 2, + "column": 13 + } + }, + "value": "textarea" + }, + { + "type": "HTMLTagClose", + "range": [ + 24, + 25 + ], + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 14 + } + }, + "value": "" + }, + { + "type": "VExpressionStart", + "range": [ + 25, + 27 + ], + "loc": { + "start": { + "line": 2, + "column": 14 + }, + "end": { + "line": 2, + "column": 16 + } + }, + "value": "{{" + }, + { + "type": "HTMLRCDataText", + "range": [ + 27, + 30 + ], + "loc": { + "start": { + "line": 2, + "column": 16 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "value": "foo" + }, + { + "type": "HTMLWhitespace", + "range": [ + 30, + 31 + ], + "loc": { + "start": { + "line": 2, + "column": 19 + }, + "end": { + "line": 2, + "column": 20 + } + }, + "value": " " + }, + { + "type": "HTMLRCDataText", + "range": [ + 31, + 34 + ], + "loc": { + "start": { + "line": 2, + "column": 20 + }, + "end": { + "line": 2, + "column": 23 + } + }, + "value": "bar" + }, + { + "type": "VExpressionEnd", + "range": [ + 34, + 36 + ], + "loc": { + "start": { + "line": 2, + "column": 23 + }, + "end": { + "line": 2, + "column": 25 + } + }, + "value": "}}" + }, + { + "type": "HTMLEndTagOpen", + "range": [ + 36, + 46 + ], + "loc": { + "start": { + "line": 2, + "column": 25 + }, + "end": { + "line": 2, + "column": 35 + } + }, + "value": "textarea" + }, + { + "type": "HTMLTagClose", + "range": [ + 46, + 47 + ], + "loc": { + "start": { + "line": 2, + "column": 35 + }, + "end": { + "line": 2, + "column": 36 + } + }, + "value": "" + }, + { + "type": "HTMLWhitespace", + "range": [ + 47, + 48 + ], + "loc": { + "start": { + "line": 2, + "column": 36 + }, + "end": { + "line": 3, + "column": 0 + } + }, + "value": "\n" + }, + { + "type": "HTMLEndTagOpen", + "range": [ + 48, + 58 + ], + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 10 + } + }, + "value": "template" + }, + { + "type": "HTMLTagClose", + "range": [ + 58, + 59 + ], + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 11 + } + }, + "value": "" + }, + { + "type": "HTMLWhitespace", + "range": [ + 59, + 60 + ], + "loc": { + "start": { + "line": 3, + "column": 11 + }, + "end": { + "line": 4, + "column": 0 + } + }, + "value": "\n" + } + ], + "comments": [], + "errors": [ + { + "message": "Unexpected token bar", + "index": 31, + "lineNumber": 2, + "column": 20 + } + ] + } +} \ No newline at end of file diff --git a/test/fixtures/ast/mustache-errors-in-textarea/source.vue b/test/fixtures/ast/mustache-errors-in-textarea/source.vue new file mode 100644 index 00000000..da4a40ab --- /dev/null +++ b/test/fixtures/ast/mustache-errors-in-textarea/source.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/ast/mustache-errors-in-textarea/token-ranges.json b/test/fixtures/ast/mustache-errors-in-textarea/token-ranges.json new file mode 100644 index 00000000..8f3b13ea --- /dev/null +++ b/test/fixtures/ast/mustache-errors-in-textarea/token-ranges.json @@ -0,0 +1,18 @@ +[ + "", + "\n ", + "", + "{{", + "foo", + " ", + "bar", + "}}", + "", + "\n", + "", + "\n" +] \ No newline at end of file diff --git a/test/fixtures/ast/mustache-errors-in-textarea/tree.json b/test/fixtures/ast/mustache-errors-in-textarea/tree.json new file mode 100644 index 00000000..f5b30cc1 --- /dev/null +++ b/test/fixtures/ast/mustache-errors-in-textarea/tree.json @@ -0,0 +1,49 @@ +[ + { + "type": "VElement", + "text": "", + "children": [ + { + "type": "VStartTag", + "text": "", + "children": [] + } + ] + } +] \ No newline at end of file diff --git a/test/index.js b/test/index.js index e4d898b3..bf60f368 100644 --- a/test/index.js +++ b/test/index.js @@ -896,6 +896,44 @@ describe("Basic tests", async () => { assert.strictEqual(messages.length, 1) assert.strictEqual(messages[0].message, "'c' is not defined.") }) + + it("should sort comments by their original source position", () => { + const code = ` + + + +` + + const result = parseForESLint(code, { sourceType: "module" }) + const comments = result.ast.comments + + // Should have 2 comments + assert.strictEqual(comments.length, 2) + + // Comments should be sorted by their original position in source code + assert.strictEqual(comments[0].type, "Line") + assert.strictEqual(comments[0].value, " first") + assert.strictEqual(comments[0].loc.start.line, 3) + + assert.strictEqual(comments[1].type, "Block") + assert.strictEqual(comments[1].value, "*\n * second\n ") + assert.strictEqual(comments[1].loc.start.line, 9) + + // Verify comments are sorted by range + assert.ok(comments[0].range[0] < comments[1].range[0]) + }) }) }) diff --git a/tsconfig.json b/tsconfig.json index e4cbfcc2..c5dc0e4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "inlineSources": true, - "lib": ["es2015"], + "lib": ["es2023"], "module": "commonjs", "moduleResolution": "node", "newLine": "LF", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy