diff --git a/apps/oxlint/src-js/package/define.ts b/apps/oxlint/src-js/package/define.ts index 59831af1bdf58..bc68bbaabd635 100644 --- a/apps/oxlint/src-js/package/define.ts +++ b/apps/oxlint/src-js/package/define.ts @@ -11,16 +11,6 @@ import type { SourceCode } from "../plugins/source_code.ts"; import type { BeforeHook, Visitor, VisitorWithHooks } from "../plugins/types.ts"; import type { SetNullable } from "../utils/types.ts"; -const { - defineProperty, - getPrototypeOf, - hasOwn, - setPrototypeOf, - create: ObjectCreate, - freeze, - assign: ObjectAssign, -} = Object; - // Empty visitor object, returned by `create` when `before` hook returns `false`. const EMPTY_VISITOR: Visitor = {}; @@ -48,7 +38,7 @@ export function definePlugin(plugin: Plugin): Plugin { // Make each rule in the plugin ESLint-compatible by calling `defineRule` on it for (const ruleName in rules) { - if (hasOwn(rules, ruleName)) { + if (Object.hasOwn(rules, ruleName)) { rules[ruleName] = defineRule(rules[ruleName]); } } @@ -91,10 +81,12 @@ export function defineRule(rule: Rule): Rule { // Copy properties from ESLint's context object to `context`. // ESLint's context object is an object of form `{ id, options, report }`, with all other properties // and methods on another object which is its prototype. - defineProperty(context, "id", { value: eslintContext.id }); - defineProperty(context, "options", { value: eslintContext.options }); - defineProperty(context, "report", { value: eslintContext.report }); - setPrototypeOf(context, getPrototypeOf(eslintContext)); + Object.defineProperties(context, { + id: { value: eslintContext.id }, + options: { value: eslintContext.options }, + report: { value: eslintContext.report }, + }); + Object.setPrototypeOf(context, Object.getPrototypeOf(eslintContext)); // If `before` hook returns `false`, skip traversal by returning an empty object as visitor if (beforeHook !== null) { @@ -119,7 +111,7 @@ let cwd: string | null = null; // All other getters/methods throw, same as they do in main implementation. // // See `FILE_CONTEXT` in `plugins/context.ts` for details of all the getters/methods. -const FILE_CONTEXT: FileContext = freeze({ +const FILE_CONTEXT: FileContext = Object.freeze({ get filename(): string { throw new Error("Cannot access `context.filename` in `createOnce`"); }, @@ -165,7 +157,7 @@ const FILE_CONTEXT: FileContext = freeze({ extend(this: FileContext, extension: Record): FileContext { // Note: We can allow calling `extend` in `createOnce`, as it involves no file-specific state - return freeze(ObjectAssign(ObjectCreate(this), extension)); + return Object.freeze(Object.assign(Object.create(this), extension)); }, get parserOptions(): Record { @@ -201,7 +193,7 @@ function createContextAndVisitor(rule: CreateOnceRule): { // Really, accessing `options` or calling `report` should throw, because they're illegal in `createOnce`. // But any such bugs should have been caught when testing the rule in Oxlint, so should be OK to take this shortcut. // `FILE_CONTEXT` prototype provides `cwd` property and `extends` method, which are available in `createOnce`. - const context: Context = ObjectCreate(FILE_CONTEXT, { + const context: Context = Object.create(FILE_CONTEXT, { id: { value: "", enumerable: true, configurable: true }, options: { value: null, enumerable: true, configurable: true }, report: { value: null, enumerable: true, configurable: true }, diff --git a/apps/oxlint/src-js/package/rule_tester.ts b/apps/oxlint/src-js/package/rule_tester.ts index 4c2d12ff60b32..aa8f0ec482e17 100644 --- a/apps/oxlint/src-js/package/rule_tester.ts +++ b/apps/oxlint/src-js/package/rule_tester.ts @@ -24,10 +24,6 @@ import type { Options } from "../plugins/options.ts"; import type { DiagnosticData, Suggestion } from "../plugins/report.ts"; import type { ParseOptions } from "./parse.ts"; -const { hasOwn } = Object, - { isArray } = Array, - jsonStringify = JSON.stringify; - // ------------------------------------------------------------------------------ // `describe` and `it` functions // ------------------------------------------------------------------------------ @@ -555,18 +551,21 @@ function assertInvalidTestCaseMessageIsCorrect( messages: Record | null, ): void { // Check `message` property - if (hasOwn(error, "message")) { + if (Object.hasOwn(error, "message")) { // Check `message` property assert( - !hasOwn(error, "messageId"), + !Object.hasOwn(error, "messageId"), "Error should not specify both `message` and a `messageId`", ); - assert(!hasOwn(error, "data"), "Error should not specify both `data` and `message`"); + assert(!Object.hasOwn(error, "data"), "Error should not specify both `data` and `message`"); assertMessageMatches(diagnostic.message, error.message!); return; } - assert(hasOwn(error, "messageId"), "Test error must specify either a `messageId` or `message`"); + assert( + Object.hasOwn(error, "messageId"), + "Test error must specify either a `messageId` or `message`", + ); // Check `messageId` property assert( @@ -575,7 +574,7 @@ function assertInvalidTestCaseMessageIsCorrect( ); const messageId: string = error.messageId!; - if (!hasOwn(messages, messageId)) { + if (!Object.hasOwn(messages, messageId)) { const legalMessageIds = `[${Object.keys(messages) .map((key) => `'${key}'`) .join(", ")}]`; @@ -607,7 +606,7 @@ function assertInvalidTestCaseMessageIsCorrect( ); } - if (hasOwn(error, "data")) { + if (Object.hasOwn(error, "data")) { // If data was provided, then directly compare the returned message to a synthetic // interpolated message using the same message ID and data provided in the test const rehydratedMessage = replacePlaceholders(ruleMessage, error.data!); @@ -644,12 +643,12 @@ function assertInvalidTestCaseLocationIsCorrect( const columnOffset = test.eslintCompat === true ? 1 : 0; - if (hasOwn(error, "line")) { + if (Object.hasOwn(error, "line")) { actualLocation.line = diagnostic.line; expectedLocation.line = error.line; } - if (hasOwn(error, "column")) { + if (Object.hasOwn(error, "column")) { actualLocation.column = diagnostic.column + columnOffset; expectedLocation.column = error.column; } @@ -666,7 +665,7 @@ function assertInvalidTestCaseLocationIsCorrect( diagnostic.endLine === diagnostic.line && diagnostic.endColumn === diagnostic.column; - if (hasOwn(error, "endLine")) { + if (Object.hasOwn(error, "endLine")) { if (error.endLine === undefined && canVoidEndLocation) { actualLocation.endLine = undefined; } else { @@ -675,7 +674,7 @@ function assertInvalidTestCaseLocationIsCorrect( expectedLocation.endLine = error.endLine; } - if (hasOwn(error, "endColumn")) { + if (Object.hasOwn(error, "endColumn")) { if (error.endColumn === undefined && canVoidEndLocation) { actualLocation.endColumn = undefined; } else { @@ -1068,7 +1067,7 @@ function setupOptions(test: TestCase): number { // Serialize to JSON and pass to `setOptions` let allOptionsJson: string; try { - allOptionsJson = jsonStringify({ options: allOptions, ruleIds: allRuleIds }); + allOptionsJson = JSON.stringify({ options: allOptions, ruleIds: allRuleIds }); } catch (err) { throw new Error(`Failed to serialize options: ${err}`); } @@ -1130,7 +1129,7 @@ function getTestName(test: TestCase): string { * @throws {*} - Value thrown by the hook function */ function runBeforeHook(test: TestCase): void { - if (hasOwn(test, "before")) runHook(test, test.before, "before"); + if (Object.hasOwn(test, "before")) runHook(test, test.before, "before"); } /** @@ -1140,7 +1139,7 @@ function runBeforeHook(test: TestCase): void { * @throws {*} - Value thrown by the hook function */ function runAfterHook(test: TestCase): void { - if (hasOwn(test, "after")) runHook(test, test.after, "after"); + if (Object.hasOwn(test, "after")) runHook(test, test.after, "after"); } /** @@ -1216,7 +1215,7 @@ function assertInvalidTestCaseIsWellFormed( `Did not specify errors for an invalid test of rule \`${ruleName}\``, ); assert( - isArray(errors), + Array.isArray(errors), `Invalid 'errors' property for invalid test of rule \`${ruleName}\`:` + `expected a number or an array but got ${errors === null ? "null" : typeof errors}`, ); @@ -1224,7 +1223,7 @@ function assertInvalidTestCaseIsWellFormed( } // `output` is optional, but if it exists it must be a string or `null` - if (hasOwn(test, "output")) { + if (Object.hasOwn(test, "output")) { assert( test.output === null || typeof test.output === "string", "Test property `output`, if specified, must be a string or null. " + @@ -1247,16 +1246,16 @@ function assertTestCaseCommonPropertiesAreWellFormed(test: TestCase): void { if (test.name) { assert(typeof test.name === "string", "Optional test case property `name` must be a string"); } - if (hasOwn(test, "only")) { + if (Object.hasOwn(test, "only")) { assert(typeof test.only === "boolean", "Optional test case property `only` must be a boolean"); } - if (hasOwn(test, "filename")) { + if (Object.hasOwn(test, "filename")) { assert( typeof test.filename === "string", "Optional test case property `filename` must be a string", ); } - if (hasOwn(test, "options")) { + if (Object.hasOwn(test, "options")) { assert(Array.isArray(test.options), "Optional test case property `options` must be an array"); } } @@ -1330,7 +1329,7 @@ function isSerializablePrimitiveOrPlainObject(value: unknown): boolean { typeof value === "string" || typeof value === "boolean" || typeof value === "number" || - (typeof value === "object" && (value.constructor === Object || isArray(value))) + (typeof value === "object" && (value.constructor === Object || Array.isArray(value))) ); } diff --git a/apps/oxlint/src-js/plugins/context.ts b/apps/oxlint/src-js/plugins/context.ts index e6d7206899afc..8d93496262566 100644 --- a/apps/oxlint/src-js/plugins/context.ts +++ b/apps/oxlint/src-js/plugins/context.ts @@ -40,8 +40,6 @@ import type { Settings } from "./settings.ts"; import type { SourceCode } from "./source_code.ts"; import type { ModuleKind, Program } from "../generated/types.d.ts"; -const { freeze, assign: ObjectAssign, create: ObjectCreate } = Object; - // Cached current working directory let cwd: string | null = null; @@ -73,13 +71,13 @@ export const ECMA_VERSION = 2026; const ECMA_VERSION_NUMBER = 17; // Supported ECMAScript versions. This matches ESLint's default. -const SUPPORTED_ECMA_VERSIONS = freeze([3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); +const SUPPORTED_ECMA_VERSIONS = Object.freeze([3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); // Singleton object for parser's `Syntax` property. Generated lazily. let Syntax: Record | null = null; // Singleton object for parser. -const PARSER = freeze({ +const PARSER = Object.freeze({ /** * Parser name. */ @@ -113,11 +111,11 @@ const PARSER = freeze({ get Syntax(): Readonly> { // Construct lazily, as it's probably rarely used if (Syntax === null) { - Syntax = ObjectCreate(null); + Syntax = Object.create(null); for (const key in visitorKeys) { Syntax![key] = key; } - freeze(Syntax); + Object.freeze(Syntax); } return Syntax!; }, @@ -135,7 +133,7 @@ const PARSER = freeze({ // Singleton object for parser options. // TODO: `sourceType` is the only property ESLint provides. But does TS-ESLint provide any further properties? -const PARSER_OPTIONS = freeze({ +const PARSER_OPTIONS = Object.freeze({ /** * Source type of the file being linted. */ @@ -211,7 +209,7 @@ if (CONFORMANCE) { }); } -freeze(LANGUAGE_OPTIONS); +Object.freeze(LANGUAGE_OPTIONS); /** * Language options used when parsing a file. @@ -245,7 +243,7 @@ export type LanguageOptions = Readonly; // TODO: When we write a rule tester, throw an error in the tester if the rule uses deprecated methods/getters. // We'll need to offer an option to opt out of these errors, for rules which delegate to another rule whose code // the author doesn't control. -const FILE_CONTEXT = freeze({ +const FILE_CONTEXT = Object.freeze({ /** * Absolute path of the file being linted. */ @@ -360,7 +358,7 @@ const FILE_CONTEXT = freeze({ */ extend(this: FileContext, extension: Record): FileContext { // Note: We can allow calling `extend` in `createOnce`, as it involves no file-specific state - return freeze(ObjectAssign(ObjectCreate(this), extension)); + return Object.freeze(Object.assign(Object.create(this), extension)); }, /** @@ -436,7 +434,7 @@ export function createContext(fullRuleName: string, ruleDetails: RuleDetails): R // IMPORTANT: Methods/getters must not use `this`, to support wrapped context objects // or e.g. `const { report } = context; report(diagnostic);`. // https://github.com/oxc-project/oxc/issues/15325 - return freeze({ + return Object.freeze({ // Inherit from `FILE_CONTEXT`, which provides getters for file-specific properties __proto__: FILE_CONTEXT, // Rule ID, in form `/` diff --git a/apps/oxlint/src-js/plugins/fix.ts b/apps/oxlint/src-js/plugins/fix.ts index 15c3ae5e9609d..e3cd9ac14ecce 100644 --- a/apps/oxlint/src-js/plugins/fix.ts +++ b/apps/oxlint/src-js/plugins/fix.ts @@ -4,11 +4,6 @@ import type { RuleDetails } from "./load.ts"; import type { Range, Ranged } from "./location.ts"; import type { Diagnostic } from "./report.ts"; -const { prototype: ArrayPrototype, from: ArrayFrom } = Array, - { getPrototypeOf, hasOwn, prototype: ObjectPrototype } = Object, - { ownKeys } = Reflect, - IteratorSymbol = Symbol.iterator; - // Type of `fix` function. // `fix` can return a single fix, an array of fixes, or any iterator that yields fixes. // e.g. `(function*() { yield fix1; yield fix2; })()` @@ -101,13 +96,13 @@ export function getFixes(diagnostic: Diagnostic, ruleDetails: RuleDetails): Fix[ if (!fixes) return null; // `fixes` can be any iterator, not just an array e.g. `fix: function*() { yield fix1; yield fix2; }` - if (IteratorSymbol in fixes) { + if (Symbol.iterator in fixes) { let isCloned = false; // Check prototype instead of using `Array.isArray()`, to ensure it is a native `Array`, // not a subclass which may have overridden `toJSON()` in a way which could make `JSON.stringify()` throw - if (getPrototypeOf(fixes) !== ArrayPrototype || hasOwn(fixes, "toJSON")) { - fixes = ArrayFrom(fixes); + if (Object.getPrototypeOf(fixes) !== Array.prototype || Object.hasOwn(fixes, "toJSON")) { + fixes = Array.from(fixes); isCloned = true; } @@ -176,12 +171,12 @@ function validateAndConformFix(fix: unknown): Fix { // If `fix` is already well-formed, return it as-is. // Note: `ownKeys(fix).length === 2` rules out `fix` having a custom `toJSON` method. - const fixPrototype = getPrototypeOf(fix); + const fixPrototype = Object.getPrototypeOf(fix); if ( - (fixPrototype === ObjectPrototype || fixPrototype === null) && - ownKeys(fix).length === 2 && - getPrototypeOf(range) === ArrayPrototype && - !hasOwn(range, "toJSON") && + (fixPrototype === Object.prototype || fixPrototype === null) && + Reflect.ownKeys(fix).length === 2 && + Object.getPrototypeOf(range) === Array.prototype && + !Object.hasOwn(range, "toJSON") && range.length === 2 && typeof text === "string" ) { diff --git a/apps/oxlint/src-js/plugins/globals.ts b/apps/oxlint/src-js/plugins/globals.ts index 76b2a0bd37263..2dd4d36165c65 100644 --- a/apps/oxlint/src-js/plugins/globals.ts +++ b/apps/oxlint/src-js/plugins/globals.ts @@ -4,9 +4,6 @@ import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; -const { freeze } = Object, - { isArray } = Array; - /** * Globals for the file being linted. * @@ -55,11 +52,11 @@ export function initGlobals(): void { // Freeze the globals object, to prevent any mutation of `globals` by plugins. // No need to deep freeze since all keys are just strings. - freeze(globals); + Object.freeze(globals); } debugAssertIsNonNull(globals); - debugAssert(typeof globals === "object" && !isArray(globals)); + debugAssert(typeof globals === "object" && !Array.isArray(globals)); } /** diff --git a/apps/oxlint/src-js/plugins/json.ts b/apps/oxlint/src-js/plugins/json.ts index b11e439a3675c..ada4c211d196f 100644 --- a/apps/oxlint/src-js/plugins/json.ts +++ b/apps/oxlint/src-js/plugins/json.ts @@ -2,9 +2,6 @@ * Methods and types related to JSON. */ -const { isArray } = Array, - { freeze } = Object; - /** * A JSON value. */ @@ -27,7 +24,7 @@ export type JsonObject = { [key: string]: JsonValue }; export function deepFreezeJsonValue(value: JsonValue): undefined { if (value === null || typeof value !== "object") return; - if (isArray(value)) { + if (Array.isArray(value)) { deepFreezeJsonArray(value); } else { deepFreezeJsonObject(value); @@ -48,7 +45,7 @@ export function deepFreezeJsonObject(obj: JsonObject): undefined { for (const key in obj) { deepFreezeJsonValue(obj[key]); } - freeze(obj); + Object.freeze(obj); } /** @@ -63,5 +60,5 @@ export function deepFreezeJsonArray(arr: JsonValue[]): undefined { for (let i = 0, len = arr.length; i !== len; i++) { deepFreezeJsonValue(arr[i]); } - freeze(arr); + Object.freeze(arr); } diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index cd6a761163aa5..12eff678c4f64 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -10,10 +10,6 @@ import type { RuleMeta } from "./rule_meta.ts"; import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from "./types.ts"; import type { SetNullable } from "../utils/types.ts"; -const ObjectKeys = Object.keys, - { isArray } = Array, - { stringify: JSONStringify, parse: JSONParse } = JSON; - /** * Linter plugin, comprising multiple rules */ @@ -140,7 +136,7 @@ export function registerPlugin(plugin: Plugin, packageName: string | null): Plug const offset = registeredRules.length; const { rules } = plugin; - const ruleNames = ObjectKeys(rules); + const ruleNames = Object.keys(rules); const ruleNamesLen = ruleNames.length; for (let i = 0; i < ruleNamesLen; i++) { @@ -166,7 +162,7 @@ export function registerPlugin(plugin: Plugin, packageName: string | null): Plug const inputDefaultOptions = ruleMeta.defaultOptions; if (inputDefaultOptions != null) { // TODO: Validate against provided options schema - if (!isArray(inputDefaultOptions)) { + if (!Array.isArray(inputDefaultOptions)) { throw new TypeError("`rule.meta.defaultOptions` must be an array if provided"); } @@ -176,7 +172,7 @@ export function registerPlugin(plugin: Plugin, packageName: string | null): Plug // or circular references. It may also be the fastest, as `JSON.parse` and `JSON.serialize` are native code. // If we move to doing options merging on Rust side, we'll need to convert to JSON anyway. try { - defaultOptions = JSONParse(JSONStringify(inputDefaultOptions)) as Options; + defaultOptions = JSON.parse(JSON.stringify(inputDefaultOptions)) as Options; } catch (err) { throw new Error( `\`rule.meta.defaultOptions\` must be JSON-serializable: ${getErrorMessage(err)}`, @@ -237,7 +233,7 @@ export function registerPlugin(plugin: Plugin, packageName: string | null): Plug // and if not, skip calling into JS entirely. In that case, the `before` hook won't get called. // We can't emulate that behavior exactly, but we can at least emulate it in this simple case, // and prevent users defining rules with *only* a `before` hook, which they expect to run on every file. - if (ObjectKeys(visitor).length === 0) { + if (Object.keys(visitor).length === 0) { beforeHook = neverRunBeforeHook; afterHook = null; } diff --git a/apps/oxlint/src-js/plugins/location.ts b/apps/oxlint/src-js/plugins/location.ts index c4ec391ed47e3..16f6ba77e2ed6 100644 --- a/apps/oxlint/src-js/plugins/location.ts +++ b/apps/oxlint/src-js/plugins/location.ts @@ -10,9 +10,6 @@ import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Node } from "./types.ts"; import type { Node as ESTreeNode } from "../generated/types.d.ts"; -const { defineProperty } = Object, - { isArray } = Array; - /** * Range of source offsets. */ @@ -259,7 +256,7 @@ export function getNodeLoc(node: Node): Location { }; // Replace `loc` getter with the calculated value - defineProperty(node, "loc", { value: loc, writable: true }); + Object.defineProperty(node, "loc", { value: loc, writable: true }); return loc; } @@ -304,7 +301,7 @@ function traverse(node: ESTreeNode): ESTreeNode { for (let keyIndex = 0, keysLen = keys.length; keyIndex < keysLen; keyIndex++) { const child = (node as unknown as Record)[keys[keyIndex]]; - if (isArray(child)) { + if (Array.isArray(child)) { // TODO: Binary search would be faster, especially for arrays of statements, which can be large for (let arrIndex = 0, arrLen = child.length; arrIndex < arrLen; arrIndex++) { const entry = child[arrIndex]; diff --git a/apps/oxlint/src-js/plugins/options.ts b/apps/oxlint/src-js/plugins/options.ts index 40468f0232f26..29f3c7bbe23c3 100644 --- a/apps/oxlint/src-js/plugins/options.ts +++ b/apps/oxlint/src-js/plugins/options.ts @@ -14,10 +14,6 @@ import { debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Writable } from "type-fest"; import type { JsonValue } from "./json.ts"; -const { freeze, hasOwn } = Object, - { isArray } = Array, - { min } = Math; - /** * Options for a rule on a file. */ @@ -58,8 +54,8 @@ export function setOptions(optionsJson: string): void { // Validate if (DEBUG) { - assert(isArray(allOptions), `options must be an array, got ${typeof allOptions}`); - assert(isArray(ruleIds), `ruleIds must be an array, got ${typeof allOptions}`); + assert(Array.isArray(allOptions), `options must be an array, got ${typeof allOptions}`); + assert(Array.isArray(ruleIds), `ruleIds must be an array, got ${typeof allOptions}`); assert.strictEqual( allOptions.length, ruleIds.length, @@ -67,7 +63,7 @@ export function setOptions(optionsJson: string): void { ); for (const options of allOptions) { - assert(isArray(options), `Elements of options must be arrays, got ${typeof options}`); + assert(Array.isArray(options), `Elements of options must be arrays, got ${typeof options}`); } for (const ruleId of ruleIds) { @@ -126,7 +122,7 @@ function mergeOptions( const defaultOptionsLength = defaultOptions.length, configOptionsLength = configOptions.length, - bothLength = min(defaultOptionsLength, configOptionsLength); + bothLength = Math.min(defaultOptionsLength, configOptionsLength); let i = 0; for (; i < bothLength; i++) { @@ -145,7 +141,7 @@ function mergeOptions( } } - return freeze(merged); + return Object.freeze(merged); } /** @@ -160,13 +156,13 @@ function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue if (configValue === null || typeof configValue !== "object") return configValue; // If config value is an array, it wins - if (isArray(configValue)) { + if (Array.isArray(configValue)) { deepFreezeArray(configValue); return configValue; } // If default value is a primitive or an array, config value wins (it's an object) - if (defaultValue === null || typeof defaultValue !== "object" || isArray(defaultValue)) { + if (defaultValue === null || typeof defaultValue !== "object" || Array.isArray(defaultValue)) { deepFreezeObject(configValue); return configValue; } @@ -177,8 +173,8 @@ function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue // Symbol properties and circular references are not possible in JSON, so no need to handle them here. // `configValue` is from JSON, so we can use a simple `for..in` loop over `configValue`. for (const key in configValue) { - // `hasOwn` not `in`, in case `key` is `"__proto__"` - if (hasOwn(defaultValue, key)) { + // `Object.hasOwn` not `in`, in case `key` is `"__proto__"` + if (Object.hasOwn(defaultValue, key)) { // `key` is an own property of both `configValue` and `defaultValue`, so must be an own property of `merged` too. // Therefore, we don't need special handling for if `key` is `"__proto__"`. // All the property reads and writes here will affect only the owned properties of these objects, @@ -189,5 +185,5 @@ function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue } } - return freeze(merged); + return Object.freeze(merged); } diff --git a/apps/oxlint/src-js/plugins/report.ts b/apps/oxlint/src-js/plugins/report.ts index 703d5201486ab..3666d822c2e0f 100644 --- a/apps/oxlint/src-js/plugins/report.ts +++ b/apps/oxlint/src-js/plugins/report.ts @@ -12,8 +12,6 @@ import type { Fix, FixFn } from "./fix.ts"; import type { RuleDetails } from "./load.ts"; import type { LineColumn, Ranged } from "./location.ts"; -const { hasOwn, keys: ObjectKeys } = Object; - /** * Diagnostic object. * Passed to `Context#report()`. @@ -95,7 +93,7 @@ export function report(diagnostic: Diagnostic, ruleDetails: RuleDetails): void { let { message, messageId } = getMessage(diagnostic, ruleDetails); // Interpolate placeholders {{key}} with data values - if (hasOwn(diagnostic, "data")) { + if (Object.hasOwn(diagnostic, "data")) { const { data } = diagnostic; if (data != null) message = replacePlaceholders(message, data); } @@ -103,7 +101,7 @@ export function report(diagnostic: Diagnostic, ruleDetails: RuleDetails): void { // TODO: Validate `diagnostic` let start: number, end: number, loc: LocationWithOptionalEnd | LineColumn | undefined; - if (hasOwn(diagnostic, "loc") && (loc = diagnostic.loc) != null) { + if (Object.hasOwn(diagnostic, "loc") && (loc = diagnostic.loc) != null) { // `loc` // Can be any of: // * `{ start: { line, column }, end: { line, column } }` @@ -113,7 +111,7 @@ export function report(diagnostic: Diagnostic, ruleDetails: RuleDetails): void { // * `{ line, column }` if (typeof loc !== "object") throw new TypeError("`loc` must be an object if provided"); - if (hasOwn(loc, "start")) { + if (Object.hasOwn(loc, "start")) { typeAssertIs(loc); start = getOffsetFromLineColumn(loc.start); end = loc.end == null ? start : getOffsetFromLineColumn(loc.end); @@ -174,7 +172,7 @@ function getMessage( diagnostic: Diagnostic, ruleDetails: RuleDetails, ): { message: string; messageId: string | null } { - if (hasOwn(diagnostic, "messageId")) { + if (Object.hasOwn(diagnostic, "messageId")) { const { messageId } = diagnostic; if (messageId != null) { return { @@ -184,7 +182,7 @@ function getMessage( } } - if (hasOwn(diagnostic, "message")) { + if (Object.hasOwn(diagnostic, "message")) { const { message } = diagnostic; if (typeof message === "string") return { message, messageId: null }; if (message != null) throw new TypeError("`message` must be a string"); @@ -208,9 +206,9 @@ function resolveMessageFromMessageId(messageId: string, ruleDetails: RuleDetails ); } - if (!hasOwn(messages, messageId)) { + if (!Object.hasOwn(messages, messageId)) { throw new Error( - `Unknown messageId '${messageId}'. Available \`messageIds\`: ${ObjectKeys(messages) + `Unknown messageId '${messageId}'. Available \`messageIds\`: ${Object.keys(messages) .map((msg) => `'${msg}'`) .join(", ")}`, ); diff --git a/apps/oxlint/src-js/plugins/selector.ts b/apps/oxlint/src-js/plugins/selector.ts index 2cb03a52e81fa..1f3d78fb467df 100644 --- a/apps/oxlint/src-js/plugins/selector.ts +++ b/apps/oxlint/src-js/plugins/selector.ts @@ -9,8 +9,6 @@ import type { Node as EsqueryNode } from "estree"; import type { Node } from "./types.ts"; import type { VisitFn } from "./visitor.ts"; -const ObjectKeys = Object.keys; - const { matches: esqueryMatches, parse: esqueryParse } = esquery; type NodeTypeId = number; @@ -19,7 +17,7 @@ type NodeTypeId = number; const ESQUERY_OPTIONS: ESQueryOptions = { nodeTypeKey: "type", visitorKeys, - fallback: (node: EsqueryNode) => ObjectKeys(node).filter(filterKey), + fallback: (node: EsqueryNode) => Object.keys(node).filter(filterKey), matchClass: (_className: unknown, _node: EsqueryNode, _ancestors: EsqueryNode[]) => false, // TODO: Is this right? }; const filterKey = (key: string) => key !== "parent" && key !== "range" && key !== "loc"; diff --git a/apps/oxlint/src-js/plugins/source_code.ts b/apps/oxlint/src-js/plugins/source_code.ts index bd0a2141f11b0..e274109af159e 100644 --- a/apps/oxlint/src-js/plugins/source_code.ts +++ b/apps/oxlint/src-js/plugins/source_code.ts @@ -29,8 +29,6 @@ import type { Token } from "./tokens.ts"; import type { BufferWithArrays, Comment, Node } from "./types.ts"; import type { ScopeManager } from "./scope.ts"; -const { max } = Math; - // Text decoder, for decoding source text from buffer const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }); @@ -224,7 +222,7 @@ export const SOURCE_CODE = Object.freeze({ const { range } = node; let start = range[0], end = range[1]; - if (beforeCount) start = max(start - beforeCount, 0); + if (beforeCount) start = Math.max(start - beforeCount, 0); if (afterCount) end += afterCount; return sourceText.slice(start, end); }, diff --git a/apps/oxlint/src-js/plugins/tokens.ts b/apps/oxlint/src-js/plugins/tokens.ts index 7c050f5a40df2..0c5fdecc60fbe 100644 --- a/apps/oxlint/src-js/plugins/tokens.ts +++ b/apps/oxlint/src-js/plugins/tokens.ts @@ -9,8 +9,6 @@ import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Comment, Node, NodeOrToken } from "./types.ts"; import type { Span } from "./location.ts"; -const { max, min } = Math; - /** * Options for various `SourceCode` methods e.g. `getFirstToken`. */ @@ -389,11 +387,11 @@ export function getTokens( } } - sliceStart = max(0, sliceStart - beforeCount); - sliceEnd = min(sliceEnd + afterCount, tokensLength); + sliceStart = Math.max(0, sliceStart - beforeCount); + sliceEnd = Math.min(sliceEnd + afterCount, tokensLength); if (typeof filter !== "function") { - return tokenList.slice(sliceStart, min(sliceStart + (count ?? sliceEnd), sliceEnd)); + return tokenList.slice(sliceStart, Math.min(sliceStart + (count ?? sliceEnd), sliceEnd)); } const allTokens: TokenOrComment[] = []; @@ -584,7 +582,7 @@ export function getFirstTokens( if (typeof filter !== "function") { if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, min(sliceStart + count, sliceEnd)); + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); } const firstTokens: TokenOrComment[] = []; @@ -773,7 +771,7 @@ export function getLastTokens( if (typeof filter !== "function") { if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(max(sliceStart, sliceEnd - count), sliceEnd); + return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd); } const lastTokens: TokenOrComment[] = []; @@ -920,7 +918,7 @@ export function getTokensBefore( // Maximum number of tokens to return const count = typeof countOptions === "number" - ? max(0, countOptions) + ? Math.max(0, countOptions) : typeof countOptions === "object" && countOptions !== null ? countOptions.count : null; @@ -1246,12 +1244,12 @@ export function getTokensBetween( } // Apply padding - sliceStart = max(0, sliceStart - padding); + sliceStart = Math.max(0, sliceStart - padding); sliceEnd += padding; if (typeof filter !== "function") { if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, min(sliceStart + count, sliceEnd)); + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); } const tokensBetween: TokenOrComment[] = []; @@ -1442,7 +1440,7 @@ export function getFirstTokensBetween( if (typeof filter !== "function") { if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(sliceStart, min(sliceStart + count, sliceEnd)); + return tokenList.slice(sliceStart, Math.min(sliceStart + count, sliceEnd)); } const firstTokens: TokenOrComment[] = []; @@ -1634,7 +1632,7 @@ export function getLastTokensBetween( // Fast path for the common case if (typeof filter !== "function") { if (typeof count !== "number") return tokenList.slice(sliceStart, sliceEnd); - return tokenList.slice(max(sliceStart, sliceEnd - count), sliceEnd); + return tokenList.slice(Math.max(sliceStart, sliceEnd - count), sliceEnd); } const tokensBetween: TokenOrComment[] = []; diff --git a/apps/oxlint/src-js/plugins/visitor.ts b/apps/oxlint/src-js/plugins/visitor.ts index 517766096cf24..b4b467f3be244 100644 --- a/apps/oxlint/src-js/plugins/visitor.ts +++ b/apps/oxlint/src-js/plugins/visitor.ts @@ -93,8 +93,6 @@ import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts"; import type { Node, Visitor } from "./types.ts"; -const ObjectKeys = Object.keys; - // Visit function for a specific AST node type. export type VisitFn = (node: Node) => void; @@ -231,7 +229,7 @@ export function addVisitorToCompiled(visitor: Visitor): void { } // Exit if is empty visitor - const keys = ObjectKeys(visitor), + const keys = Object.keys(visitor), keysLen = keys.length; if (keysLen === 0) return; diff --git a/apps/oxlint/src-js/utils/globals.ts b/apps/oxlint/src-js/utils/globals.ts new file mode 100644 index 0000000000000..502bd05774c45 --- /dev/null +++ b/apps/oxlint/src-js/utils/globals.ts @@ -0,0 +1,33 @@ +/** + * Properties of global objects exported as variables. + * + * TSDown will replace e.g. `Object.keys` with import of `ObjectKeys` from this file. + * + * If you use any globals in code in `src-js` directory, you should add them to this file. + * + * See TSDown config file for more details. + */ + +export const { + prototype: ObjectPrototype, + hasOwn: ObjectHasOwn, + keys: ObjectKeys, + values: ObjectValues, + freeze: ObjectFreeze, + defineProperty: ObjectDefineProperty, + defineProperties: ObjectDefineProperties, + create: ObjectCreate, + assign: ObjectAssign, + getPrototypeOf: ObjectGetPrototypeOf, + setPrototypeOf: ObjectSetPrototypeOf, +} = Object; + +export const { prototype: ArrayPrototype, isArray: ArrayIsArray, from: ArrayFrom } = Array; + +export const { min: MathMin, max: MathMax, floor: MathFloor } = Math; + +export const { parse: JSONParse, stringify: JSONStringify } = JSON; + +export const { ownKeys: ReflectOwnKeys } = Reflect; + +export const { iterator: SymbolIterator } = Symbol; diff --git a/apps/oxlint/tsdown.config.ts b/apps/oxlint/tsdown.config.ts index 84a1bfe8134cc..39b1b4bc6398c 100644 --- a/apps/oxlint/tsdown.config.ts +++ b/apps/oxlint/tsdown.config.ts @@ -1,4 +1,5 @@ -import { join } from "node:path"; +import fs from "node:fs"; +import { join as pathJoin, relative as pathRelative, dirname } from "node:path"; import { defineConfig } from "tsdown"; import { parseSync, Visitor } from "oxc-parser"; @@ -26,6 +27,9 @@ const commonConfig = defineConfig({ fixedExtension: false, }); +const plugins = [createReplaceGlobalsPlugin()]; +if (!DEBUG) plugins.push(createReplaceAssertsPlugin()); + export default defineConfig([ // Main build { @@ -50,9 +54,9 @@ export default defineConfig([ DEBUG: DEBUG ? "true" : "false", CONFORMANCE: CONFORMANCE ? "true" : "false", }, - plugins: DEBUG ? [] : [createReplaceAssertsPlugin()], + plugins, inputOptions: { - // For `replaceAssertsPlugin` + // For `replaceAssertsPlugin` and `replaceGlobalsPlugin` experimental: { nativeMagicString: true }, }, }, @@ -115,7 +119,7 @@ export default defineConfig([ * @returns Plugin */ function createReplaceAssertsPlugin(): Plugin { - const ASSERTS_PATH = join(import.meta.dirname, "src-js/utils/asserts.ts"); + const ASSERTS_PATH = pathJoin(import.meta.dirname, "src-js/utils/asserts.ts"); return { name: "replace-asserts", @@ -185,3 +189,191 @@ function createReplaceAssertsPlugin(): Plugin { }, }; } + +// prettier-ignore +const GLOBALS = new Set([ + "Object", "Array", "Math", "JSON", "Reflect", "Symbol", "Function", "Number", "Boolean", "String", "Date", "Promise", + "RegExp", "BigInt", "Map", "Set", "Error", "AggregateError", "EvalError", "RangeError", "ReferenceError", + "SyntaxError", "TypeError", "URIError", "Buffer", "ArrayBuffer", "SharedArrayBuffer", "Atomics", "Uint8Array", + "Int8Array", "Uint16Array", "Int16Array", "Uint32Array", "Int32Array", "BigUint64Array", "BigInt64Array", + "Uint8ClampedArray", "Float32Array", "Float64Array", "Float16Array", "DataView", "WebAssembly", "Iterator", + "WeakMap", "WeakSet", "Proxy", "FinalizationRegistry", "WeakRef", "URL", "URLSearchParams", "TextEncoder", + "TextDecoder", "BroadcastChannel", "MessageChannel", "MessagePort", "Blob", "File" +]); + +/** + * Create a plugin to replace usage of properties of globals with global vars defined in `utils/globals.ts`. + * + * This more performant, due to reduced property lookups, and minifies better. + * + * ```ts + * // Original code + * const keys = Object.keys(obj); + * + * // After transform + * import { ObjectKeys } from "../utils/globals.ts"; + * const keys = ObjectKeys(obj); + * ``` + * + * If TSDown produces any errors about missing imports, likely you need to add the missing global(s) + * to `utils/globals.ts`. + */ +function createReplaceGlobalsPlugin(): Plugin { + // Path to file which exports global vars + const GLOBALS_PATH = pathJoin(import.meta.dirname, "src-js/utils/globals.ts"); + + // Parse the file to get the list of global vars it exports + const availableGlobals = getAvailableGlobals(GLOBALS_PATH); + + return { + name: "replace-globals", + transform: { + // Only process TS files in `src-js` directory + filter: { id: /\/src-js\/.+\.ts$/ }, + + async handler(code, path, meta) { + const magicString = meta.magicString!; + const { program, errors } = parseSync(path, code); + if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`); + + // Visit AST and replace all references to globals with top-level vars + const varNames = new Set(), + visitedMemberExpressions = new Set(), + missingGlobalVars = new Set(); + + const visitor = new Visitor({ + MemberExpression(node) { + // Skip nested `MemberExpression`s e.g. `Object.prototype` in `Object.prototype.toString` + if (visitedMemberExpressions.has(node)) return; + + // Exit if computed (`obj[prop]`) or private property (`obj.#prop`). + let { object, property } = node; + if (node.computed || property.type !== "Identifier") return; + + // Gather all properties in reverse order. + // e.g. `Object.prototype.toString` -> `propNames = ["toString", "prototype"]`. + const propNames: string[] = [property.name]; + while (true) { + // If `object` is an identifier, `node` is a member expression of form `a.b`, `a.b.c`, etc. + if (object.type === "Identifier") break; + + // If `object` is not a member expression, exit e.g. `foo().x` + if (object.type !== "MemberExpression") return; + + // We can't handle deep nesting yet + // oxlint-disable-next-line no-constant-condition + if (1) return; + + // Avoid processing the nested member expression again when it's visited later + visitedMemberExpressions.add(object); + + // Exit if computed (`obj[prop]`) or private property (`obj.#prop`). + property = object.property; + if (object.computed || property.type !== "Identifier") return; + + // `node` of form `.a.b` or `.a.b.c`. + // Loop round to process the `` part. + propNames.push(property.name); + + object = object.object; + } + + // Found a member expression of form `obj.a`, or `obj.a.b`, `obj.a.b.c`, etc. + // Exit if `obj` is not a global. + const globalName = object.name; + if (!GLOBALS.has(globalName)) return; + + const propName = propNames.reverse().join("."); + + const mapping = availableGlobals.get(globalName); + if (!mapping) { + missingGlobalVars.add(`\`${object.name}.${propName}\``); + return; + } + + const varName = mapping.get(propName); + if (!varName) { + missingGlobalVars.add(`\`${object.name}.${propName}\``); + return; + } + + // Add var name (e.g. `ObjectHasOwn`) to set of vars to import + varNames.add(varName); + + // Replace `Object.hasOwn` with `ObjectHasOwn` + magicString.overwrite(node.start, node.end, varName); + }, + }); + visitor.visit(program); + + // Log any globals that were not converted because `utils/globals.ts` has no export for them + if (missingGlobalVars.size > 0) { + // oxlint-disable-next-line no-console + console.error( + "--------------------------------------------------------------------------------\n" + + `WARNING: Unable to convert ${[...missingGlobalVars].join(" or ")} to global vars.\n` + + `Add exports to \`utils/globals.ts\` for them.\n` + + "--------------------------------------------------------------------------------", + ); + } + + if (varNames.size === 0) return; + + // Some globals were found. Import them from `utils/globals.ts`. + const relativePath = pathRelative(dirname(path), GLOBALS_PATH); + const importStmt = `import { ${[...varNames].join(", ")} } from ${JSON.stringify(relativePath)};\n`; + + magicString.prepend(importStmt); + + return { code: magicString }; + }, + }, + }; +} + +/** + * Parse `utils/globals.ts` and return a list of globals and global vars it exports. + * @param path - Path to `utils/globals.ts` + * @returns Mapping from global name (e.g. `Object`) to mapping of properties of that global to var names + * (e.g. `hasOwn` -> `ObjectHasOwn`). + */ +function getAvailableGlobals(path: string): Map> { + const code = fs.readFileSync(path, "utf8"); + const { program, errors } = parseSync(path, code); + if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`); + + const globals = new Map>(); + + const visitor = new Visitor({ + ExportNamedDeclaration(node) { + const { declaration } = node; + if (declaration == null || declaration.type !== "VariableDeclaration") return; + const declarator = declaration.declarations[0]; + if (!declarator) return; + const { init } = declarator; + if (!init || init.type !== "Identifier") return; + + const obj = declarator.id; + if (obj.type !== "ObjectPattern") return; + + const globalName = init.name; + let mapping = globals.get(globalName); + if (!mapping) { + mapping = new Map(); + globals.set(globalName, mapping); + } + + for (const prop of obj.properties) { + if (prop.type !== "Property" || prop.method || prop.computed) continue; + + const { key, value } = prop; + if (key.type !== "Identifier" || value.type !== "Identifier") continue; + + mapping.set(key.name, value.name); + } + }, + }); + visitor.visit(program); + + return globals; +}