Skip to content

Commit 2f9735d

Browse files
feat(linter/plugins): implement context.languageOptions (#15486)
Implement `Context#languageOptions`.
1 parent ac95e7d commit 2f9735d

File tree

9 files changed

+174
-4
lines changed

9 files changed

+174
-4
lines changed

apps/oxlint/src-js/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts';
33
import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts';
44

55
export type * as ESTree from './generated/types.d.ts';
6-
export type { Context, Diagnostic, DiagnosticBase, DiagnosticWithLoc, DiagnosticWithNode } from './plugins/context.ts';
6+
export type {
7+
Context,
8+
Diagnostic,
9+
DiagnosticBase,
10+
DiagnosticWithLoc,
11+
DiagnosticWithNode,
12+
LanguageOptions,
13+
} from './plugins/context.ts';
714
export type { Fix, Fixer, FixFn } from './plugins/fix.ts';
815
export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts';
916
export type {

apps/oxlint/src-js/plugins/context.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@
2828

2929
import { getFixes } from './fix.js';
3030
import { getOffsetFromLineColumn } from './location.js';
31-
import { SOURCE_CODE } from './source_code.js';
31+
import { ast, initAst, SOURCE_CODE } from './source_code.js';
3232
import { settings, initSettings } from './settings.js';
3333

3434
import type { Fix, FixFn } from './fix.ts';
3535
import type { RuleDetails } from './load.ts';
3636
import type { SourceCode } from './source_code.ts';
3737
import type { Location, Ranged } from './types.ts';
38+
import type { ModuleKind } from '../generated/types.d.ts';
3839

3940
const { hasOwn, keys: ObjectKeys, freeze, assign: ObjectAssign, create: ObjectCreate } = Object;
4041

@@ -94,6 +95,70 @@ export function resetFileContext(): void {
9495
filePath = null;
9596
}
9697

98+
// ECMAScript version. This matches ESLint's default.
99+
const ECMA_VERSION = 2026;
100+
101+
// Singleton object for parser options.
102+
const PARSER_OPTIONS = freeze({
103+
/**
104+
* Source type of the file being linted.
105+
*/
106+
get sourceType(): ModuleKind {
107+
// TODO: Would be better to get `sourceType` without deserializing whole AST,
108+
// in case it's used in `create` to return an empty visitor if wrong type.
109+
// TODO: ESLint also has `commonjs` option.
110+
if (ast === null) initAst();
111+
return ast.sourceType;
112+
},
113+
});
114+
115+
// Singleton object for language options.
116+
const LANGUAGE_OPTIONS = freeze({
117+
/**
118+
* Source type of the file being linted.
119+
*/
120+
get sourceType(): ModuleKind {
121+
// TODO: Would be better to get `sourceType` without deserializing whole AST,
122+
// in case it's used in `create` to return an empty visitor if wrong type.
123+
// TODO: ESLint also has `commonjs` option.
124+
if (ast === null) initAst();
125+
return ast.sourceType;
126+
},
127+
128+
/**
129+
* ECMAScript version of the file being linted.
130+
*/
131+
ecmaVersion: ECMA_VERSION,
132+
133+
/**
134+
* Parser used to parse the file being linted.
135+
*/
136+
get parser(): Record<string, unknown> {
137+
throw new Error('`context.languageOptions.parser` is not implemented yet.'); // TODO
138+
},
139+
140+
/**
141+
* Parser options used to parse the file being linted.
142+
*/
143+
parserOptions: PARSER_OPTIONS,
144+
145+
/**
146+
* Globals defined for the file being linted.
147+
*/
148+
// ESLint has `globals` as `null`, not empty object, if no globals are defined.
149+
get globals(): Record<string, 'readonly' | 'writable' | 'off'> | null {
150+
// TODO: Get globals from Rust side.
151+
// Note: ESLint's type is "writable", whereas Oxlint's is "writeable" (misspelled with extra "e").
152+
// Probably we should fix that on Rust side (while still allowing "writeable").
153+
return null;
154+
},
155+
});
156+
157+
/**
158+
* Language options used when parsing a file.
159+
*/
160+
export type LanguageOptions = typeof LANGUAGE_OPTIONS;
161+
97162
// Singleton object for file-specific properties.
98163
//
99164
// Only one file is linted at a time, so we reuse a single object for all files.
@@ -140,9 +205,9 @@ const FILE_CONTEXT = freeze({
140205
/**
141206
* Language options used when parsing this file.
142207
*/
143-
get languageOptions(): Record<string, unknown> {
208+
get languageOptions(): LanguageOptions {
144209
if (filePath === null) throw new Error('Cannot access `context.languageOptions` in `createOnce`');
145-
throw new Error('`context.languageOptions` is not implemented yet.'); // TODO
210+
return LANGUAGE_OPTIONS;
146211
},
147212

148213
/**

apps/oxlint/test/e2e.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ describe('oxlint CLI', () => {
190190
await testFixture('sourceCode_scope_methods');
191191
});
192192

193+
it('should support languageOptions', async () => {
194+
await testFixture('languageOptions');
195+
});
196+
193197
it('should support selectors', async () => {
194198
await testFixture('selector');
195199
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": {
4+
"correctness": "off"
5+
},
6+
"rules": {
7+
"language-options-plugin/lang": "error"
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x language-options-plugin(lang): languageOptions:
7+
| sourceType: script
8+
| ecmaVersion: 2026
9+
| parserOptions: {"sourceType":"script"}
10+
| globals: null
11+
,-[files/index.cjs:1:1]
12+
1 | let x;
13+
: ^
14+
`----
15+
16+
x language-options-plugin(lang): languageOptions:
17+
| sourceType: module
18+
| ecmaVersion: 2026
19+
| parserOptions: {"sourceType":"module"}
20+
| globals: null
21+
,-[files/index.js:1:1]
22+
1 | let x;
23+
: ^
24+
`----
25+
26+
x language-options-plugin(lang): languageOptions:
27+
| sourceType: module
28+
| ecmaVersion: 2026
29+
| parserOptions: {"sourceType":"module"}
30+
| globals: null
31+
,-[files/index.mjs:1:1]
32+
1 | let x;
33+
: ^
34+
`----
35+
36+
Found 0 warnings and 3 errors.
37+
Finished in Xms on 3 files using X threads.
38+
```
39+
40+
# stderr
41+
```
42+
WARNING: JS plugins are experimental and not subject to semver.
43+
Breaking changes are possible while JS plugins support is under development.
44+
```
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Plugin, Node } from '../../../dist/index.js';
2+
3+
const SPAN: Node = {
4+
start: 0,
5+
end: 0,
6+
range: [0, 0],
7+
loc: {
8+
start: { line: 0, column: 0 },
9+
end: { line: 0, column: 0 },
10+
},
11+
};
12+
13+
const plugin: Plugin = {
14+
meta: {
15+
name: 'language-options-plugin',
16+
},
17+
rules: {
18+
lang: {
19+
create(context) {
20+
const { languageOptions } = context;
21+
22+
context.report({
23+
message:
24+
'languageOptions:\n' +
25+
`sourceType: ${languageOptions.sourceType}\n` +
26+
`ecmaVersion: ${languageOptions.ecmaVersion}\n` +
27+
`parserOptions: ${JSON.stringify(languageOptions.parserOptions)}\n` +
28+
`globals: ${JSON.stringify(languageOptions.globals)}`,
29+
node: SPAN,
30+
});
31+
32+
return {};
33+
},
34+
},
35+
},
36+
};
37+
38+
export default plugin;

0 commit comments

Comments
 (0)