Fix an incompatibility with `eslint-plugin-jsdoc`. It relies on properties of `context` being values rather than getters:
https://github.com/gajus/eslint-plugin-jsdoc/blob/26276d428631a2625575607cba7875dab6797091/src/buildForbidRuleDefinition.js#L86-L103
When `options` property is a getter, this code throws an error because it attempts to define a property with both `value` and `get` in its descriptor.
This PR replaces the `id` and `options` getters with value properties.
The getters were there to prevent the `Context` object from being mutated by rule code, and allow the object to be frozen, as it is in ESLint. But each rule gets its own `Context` object, so if it chooses to mutate it, it only harms itself, and can't impact other rules/plugins. In ESLint, trying to alter the frozen `Context` object would be a hard error, so it's doubtful that any ESLint plugins do this anyway.
This is no real downside to this change for us, so we might as well make the change, and fix `eslint-plugin-jsdoc` and any other plugins which may depend on ESLint's behavior here.
Add correct version for `context.languageOptions.parser.version`.
Blocker previously was that version should match `oxc-parser` package, which is not entirely trivial, as we release NAPI packages and Oxlint separately.
Work around this by giving `"oxlint"` as name of the parser, so we can use Oxlint version.
TSDown extracts the `version` field from `package.json` without including the whole contents of `package.json` in the bundled code.
Follow on after #19193. Refactor.
That PR introduced support for legacy call styles for `context.report`.
Move that implementation into `report.ts`, shorten the code a little, and produce consistent object shapes. Avoid calling `convertLegacyCallArgs` in the common case that `report` is called in the modern way.
Workspaces support (#17809) added overhead on JS side of hashmap lookups. Every call to `lintFile` has to look up details (e.g. registered rules, rule options) from hashmaps keyed by workspace URI.
### Perf
Take a different approach which is optimized for common case of there being only 1 workspace:
* At any time there's 1 "current workspace".
* Store rules, options, and CWD in top-level vars (back to how it was before #17809).
* Switch workspaces only when required. Switching reassigns the values in these top level vars.
Before:
* Get rules, options, and CWD from hashmaps keyed by workspace URI.
After:
* Quick check "is the current workspace the one we need?".
* If so, do nothing - the top-level vars `registeredRules`, `allOptions`, and `cwd` elready contain the right settings for the desired workspace.
* If current workspace is not the right one, look up the details for desired workspace in `workspaces` hash map and write them into the top-level vars.
In CLI, `workspaceUri` passed to `lintFile` etc is `null`, so the quick "in the right workspace?" check is particularly cheap - just `null === null`. So this reduces the overhead that workspace support adds to CLI to just that 1 instruction.
Add of these functions are entirely synchronous, so there's no possibility of a race where 1 call switches out the values in the top-level vars, while another call is busy using them.
### CLI: No call to `createWorkspace`
CLI now doesn't need to call `createWorkspace` at all. The "are we in right workspace?" check always succeeds, so it's irrelevant whether a workspace has been created or not - `workspaces` hashmap is never accessed.
So workspaces is now an LSP-only thing.
### Bundle
This PR also removes all imports from other files from `src-js/workspace/index.ts`. It now only imports `debugAssert`, which is optimized out in release build., leaving no imports at all.
Previously, the imports from other files were causing most of the JS plugins code to get loaded on first call to `createWorkspace`, which broke the lazy loading optimization - Oxlint CLI and LSP were both loading all the JS plugins code eagerly even if the user doesn't use JS plugins.
With this change, the lazy loading optimization is restored. This is visible in the conformance snapshot change in this PR.
Pass `cwd` into `setupFileContext`, so it can be set per file, rather than being a global setting.
This is preamble to #17809. This PR is some of the changes from that one split off, so the diff on #17809 is focused just on that one change.
This PR does nothing in itself, as currently `process.cwd()` is always passed to `setupFileContext`.
In ES3, directives didn't exist yet, and therefore `"use strict"` is treated as a standard string literal, rather than a directive.
In conformance tests, when test case specifies ES3, walk the AST and set `directive` property of all `ExpressionStatement`s to `null`. This transforms any directives in AST into plain string literals.
Combined with #18632 which fixed `"use strict"` processing in TS-ESLint, this means there are 8 ESLint test cases we no longer have to skip. More importantly, we're less likely to run into erroneous test failures when testing other plugins, which are laborious to diagnose.
Add `ecmaFeatures` property to `context.languageOptions.parserOptions`, with `globalReturn` and `impliedStrict` properties.
In normal operation, these are set according to the source type. In conformance build, they can be overridden by config.
Supporting JS plugins in LSP requires that different workspaces have a different value for `Context#cwd`.
As a first step, prevent accessing `cwd` property, or calling `getCwd` method in `createOnce`. Later on, we can make `cwd` a dynamic property.
Previously `context.languageOptions.globals` was `null` if no globals defined in config. This was a slight incompatibility with ESLint, which doesn't define `globals` property at all on `languageOptions` in this case.
Instead make `languageOptions.globals` an empty object. This should make no practical difference from what ESLint does, but avoids errors in rules if they do something like this:
```js
if (languageOptions.globals !== undefined) {
// This would throw if `globals` was `null` (as it used to be)
const hasWindow = languageOptions.globals.window !== "off";
}
```
Previously we had a pattern of destructuring properties of global objects into top-level vars. e.g.:
```js
const { keys: ObjectKeys, hasOwn } = Object,
{ isArray } = Array;
```
Doing this is more performant, but it makes the code harder to read.
Instead, write code normally, and use a TSDown plugin to convert:
```js
const keys = Object.keys(obj);
```
to:
```js
import { ObjectKeys } from "../utils/globals.ts";
const keys = ObjectKeys(obj);
```
Defining all the destructured globals in a single file (`utils/globals.ts`) is advantageous for bundle size. If the TSDown plugin created top level `const ObjectKeys = Object.keys;` statements in each file, then you can end up with this in bundle as minifier doesn't de-duplicate:
```js
const ObjectKeys = Object.keys;
const ObjectKeys$1 = Object.keys;
const ObjectKeys$2 = Object.keys;
```
The globals we use have to be manually added to `utils/globals.ts`, but TSDown plugin warns when this needs to be done, and doesn't generate a broken build in the meantime.
This plugin is run in debug builds too, so that the tests will catch any bugs.
In conformance tester, populate `context.languageOptions.ecmaVersion` from config/test case. Some rules alter behavior depending on `ecmaVersion`, so we need to set it to pass some of those tests.
This check is unnecessary as in order to obtain `languageOptions`, you have to go through `context.languageOptions` getter which already does the check.
If user hold onto the returned `languageOptions` object and accesses `globals` on it in a tick in between linting files, they'll just get `null` anyway.
- This PR implements a naive, inefficient solution to #16359.
- To establish current and expected behavior, this PR serializes
rust-resolved and -overridden globals once for every file.
## Future work
- [ ] Add test for globals from the `env` field. Implement support if
not currently supported.
- [ ] Optimization: we want to serialize `globals` for all files only
once at the start of the lint run.
## AI Usage
- This PR was drafted by Claude Sonnet 4.5 in Plan + Agent mode, then
manually reviewed and iterated on.
---------
Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
Implement `context.languageOptions.parser`. The `parse` function currently throws an "unimplemented" error, but it's still useful to have the other properties available to plugins.
Preparatory work for supporting rule options.
Store options for all rules in an `allOptions` array. Currently this array only contains a single value for empty options, but in future we'll populate it with options extracted from configs, sent over from Rust side.
Am adding this now, before full implementation (#14825), as it's required for rule tester.
Rename assertion functions to clarify that they're not "real" assertions which we can rely on for validating input, and are only for compile-time testing or debug assertions.
`typeAssertIs` is not a great name, but I couldn't come up with anything better. `debugAssertIs` would be not be accurate, as it'd imply that in debug builds in performs an actual assertion, which it doesn't.
In any case, we should replace `typeAssertIs` with specialized assertion functions which do actually perform runtime assertions in debug builds (e.g. `typeAssertIs<Fix>()` -> `debugAssertIsFix()`).
Pure refactor. Split up `plugins/utils.ts` into 3 files in `utils` directory.
Most importantly, assertion functions are by themselves in an `asserts.ts` file. This paves the way for the next PR (#15946).
Adapted from #15868. Enable `strictNullChecks` in TSConfig for `apps/oxlint`.
Rather than using `!` all over the place, I've added a function `assertIsNonNull`, which performs the same function. Idea is that we can later on produce a debug build where `assertIsNonNull` does an actual runtime assertion (rather than just a TS `asserts`) that the value is not `null` or `undefined`. We can use the debug build in tests, and catch any logic errors.
Unfortunately, TypeScript doesn't seem able to track side effects, so there seems to be no way to tell it "after calling `initAst()`, `ast` is always non-null". Don't want to switch to a having a `getAst` function which returns a non-null value, as that'd introduce unnecessary temp vars, and function calls, on hot paths. So this seems to be the best we can do without incurring runtime cost.
Nit. As the pedantic Mr Copilot pointed out in https://github.com/oxc-project/oxc/pull/15637#discussion_r2518858637, the pattern we use in error messages is "not implemented", not "is not implemented". Make the error message in `languageOptions.parser` getter consistent with this pattern.
Implement `parserOptions` getter on `Context` object.
This getter is deprecated by ESLint, but we have decided to implement deprecated methods/getters, to support as many existing ESLint plugins as possible (see #15606).
Implement 4 methods on `Context` which are present in ESLint, but deprecated.
* `getFilename`
* `physicalFilename`
* `getCwd`
* `getSourceCode`
After some discussion, @camc314 and I concluded that we should add these methods despite their deprecation. The reasons behind that decision are documented in a comment in this PR.
Revert #15447. It's easy to allow `Context#id` to be accessed in `createOnce` when running rule in Oxlint, but not possible to do in ESLint compat mode with current API, because `id` isn't a property of the rule, so `defineRule` doesn't know it.
We could support it by making `definePlugin` compulsory for ESLint interop, and get it to set `id` property on `Context` objects properly.
But that's a bit tricky, so just disable access for now. It's probably not very useful anyway - a rule should know what its own name is!
#15301 made `Context#cwd` available in `createOnce`. Make it available in ESLint compat mode too. Ditto the `extend` method added in #15477.
Define all other `Context` methods/getters on prototype of `Context` in ESLint compat mode, for completeness.
Fix#15325.
Some ESLint rules create a wrapped copy of `context`, in order to patch `report` and pass that wrapped context object to another rule.
Some rules also rely on unspecified details of ESLint's implementation - that `context` is formed of 2 layers - top layer being `RuleContext` (`id`, `report` etc), and bottom layer being `FileContext` (`filename`, `sourceText` etc).
This usage was broken in our implementation because:
1. We collapsed the 2 layers into 1.
2. We used a `Context` class which stored its internal data in a private property. When `context.report` was called with a different `this`, it failed because the private property couldn't be accessed from an object which isn't an instance of the class.
Fix this problem by:
1. Splitting `Context` into 2 layers like ESLint does.
2. Not using classes or private properties.
3. No methods/getters on context objects use `this`.
We now have:
* Separate `Context` object per rule, containing properties related to the rule - a plain object which uses data from closure scope.
* All rule `Context`s inherit from the same singleton `FILE_CONTEXT` object, which provides getters for properties related to the file (same for all rules).
Tests adapted from #15326.
Previously we blocked accessing `context.id` (rule name) in `createOnce`. There's no need to be so restrictive, as it's a static value, and not file-specific. Relax this restriction.
Follow-on after #14724. Deserialize and deep freeze settings lazily on demand. These operations are not so expensive, but if settings object is large, they're not so cheap either. So avoid that work unless `context.settings` is accessed.
A better eventual solution will be to send all settings objects over to JS in one go after parsing all configs, rather than again and again for every file, but this is a simple optimization to get a bit of gain in the meantime.