Skip to content

Commit a40d8c6

Browse files
authored
feat: patch missing context and SourceCode methods for v10 (#311)
1 parent 0897d95 commit a40d8c6

File tree

3 files changed

+678
-23
lines changed

3 files changed

+678
-23
lines changed

packages/compat/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## Overview
44

5-
This packages contains functions that allow you to wrap existing ESLint rules, plugins, and configurations that were intended for use with ESLint v8.x to allow them to work as-is in ESLint v9.x.
5+
This package contains functions that allow you to wrap existing ESLint rules, plugins, and configurations that were intended for use with ESLint v8.x or v9.x to allow them to work as-is in ESLint v9.x and v10.x.
66

7-
**Note:** All plugins are not guaranteed to work in ESLint v9.x. This package fixes the most common issues but can't fix everything.
7+
**Note:** All plugins are not guaranteed to work in ESLint v9.x or v10.x. This package fixes the most common issues but can't fix everything.
88

99
## Installation
1010

@@ -37,7 +37,7 @@ This package exports the following functions in both ESM and CommonJS format:
3737

3838
### Fixing Rules
3939

40-
If you have a rule that you'd like to make compatible with ESLint v9.x, you can do so using the `fixupRule()` function:
40+
If you have a rule that you'd like to make compatible with ESLint v9.x or v10.x, you can do so using the `fixupRule()` function:
4141

4242
```js
4343
// ESM example
@@ -71,7 +71,7 @@ module.exports = compatRule;
7171

7272
### Fixing Plugins
7373

74-
If you are using a plugin in your `eslint.config.js` that is not yet compatible with ESLint 9.x, you can wrap it using the `fixupPluginRules()` function:
74+
If you are using a plugin in your `eslint.config.js` that is not yet compatible with ESLint v9.x or v10.x, you can wrap it using the `fixupPluginRules()` function:
7575

7676
```js
7777
// eslint.config.js - ESM example
@@ -115,7 +115,7 @@ module.exports = defineConfig([
115115

116116
### Fixing Configs
117117

118-
If you are importing other configs into your `eslint.config.js` that use plugins that are not yet compatible with ESLint 9.x, you can wrap the entire array or a single object using the `fixupConfigRules()` function:
118+
If you are importing other configs into your `eslint.config.js` that use plugins that are not yet compatible with ESLint v9.x or v10.x, you can wrap the entire array or a single object using the `fixupConfigRules()` function:
119119

120120
```js
121121
// eslint.config.js - ESM example

packages/compat/src/fixup-rules.js

Lines changed: 212 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @filedescription Functions to fix up rules to provide missing methods on the `context` object.
2+
* @fileoverview Functions to fix up rules to provide missing methods on the `context` and `sourceCode` objects.
33
* @author Nicholas C. Zakas
44
*/
55

@@ -70,13 +70,67 @@ const fixedUpPluginReplacements = new WeakMap();
7070
*/
7171
const fixedUpPlugins = new WeakSet();
7272

73+
//-----------------------------------------------------------------------------
74+
// Helpers
75+
//-----------------------------------------------------------------------------
76+
77+
/**
78+
* Determines if two nodes or tokens overlap.
79+
* @param {object} first The first node or token to check.
80+
* @param {object} second The second node or token to check.
81+
* @returns {boolean} True if the two nodes or tokens overlap.
82+
*/
83+
function nodesOrTokensOverlap(first, second) {
84+
return (
85+
(first.range[0] <= second.range[0] &&
86+
first.range[1] >= second.range[0]) ||
87+
(second.range[0] <= first.range[0] && second.range[1] >= first.range[0])
88+
);
89+
}
90+
91+
/**
92+
* Checks whether a node is an export declaration.
93+
* @param {object} node An AST node.
94+
* @returns {boolean} True if the node is an export declaration.
95+
*/
96+
function looksLikeExport(node) {
97+
return (
98+
node.type === "ExportDefaultDeclaration" ||
99+
node.type === "ExportNamedDeclaration"
100+
);
101+
}
102+
103+
/**
104+
* Checks for the presence of a JSDoc comment for the given node and returns it.
105+
* @param {object} node The AST node to get the comment for.
106+
* @param {object} sourceCode A SourceCode instance to get comments.
107+
* @returns {object|null} The Block comment token containing the JSDoc comment
108+
* for the given node or null if not found.
109+
*/
110+
function findJSDocComment(node, sourceCode) {
111+
const tokenBefore = sourceCode.getTokenBefore(node, {
112+
includeComments: true,
113+
});
114+
115+
if (
116+
tokenBefore &&
117+
tokenBefore.type === "Block" &&
118+
tokenBefore.value.charAt(0) === "*" &&
119+
node.loc.start.line - tokenBefore.loc.end.line <= 1
120+
) {
121+
return tokenBefore;
122+
}
123+
124+
return null;
125+
}
126+
73127
//-----------------------------------------------------------------------------
74128
// Exports
75129
//-----------------------------------------------------------------------------
76130

77131
/**
78132
* Takes the given rule and creates a new rule with the `create()` method wrapped
79-
* to provide the missing methods on the `context` object.
133+
* to provide missing methods on the `context` and `sourceCode` objects.
80134
* @param {FixupRuleDefinition|FixupLegacyRuleDefinition} ruleDefinition The rule to fix up.
81135
* @returns {FixupRuleDefinition} The fixed-up rule.
82136
*/
@@ -98,52 +152,193 @@ export function fixupRule(ruleDefinition) {
98152
: ruleDefinition.create.bind(ruleDefinition);
99153

100154
function ruleCreate(context) {
101-
// if getScope is already there then no need to create old methods
155+
const sourceCode = context.sourceCode;
156+
157+
// No need to create old methods for ESLint < 9
102158
if ("getScope" in context) {
103159
return originalCreate(context);
104160
}
105161

106-
const sourceCode = context.sourceCode;
107-
let currentNode = sourceCode.ast;
162+
let eslintVersion = 9;
163+
if (!("getCwd" in context)) {
164+
eslintVersion = 10;
165+
}
166+
167+
let compatSourceCode = sourceCode;
168+
if (eslintVersion >= 10) {
169+
compatSourceCode = Object.assign(Object.create(sourceCode), {
170+
getTokenOrCommentBefore(node, skip) {
171+
return sourceCode.getTokenBefore(node, {
172+
includeComments: true,
173+
skip,
174+
});
175+
},
176+
getTokenOrCommentAfter(node, skip) {
177+
return sourceCode.getTokenAfter(node, {
178+
includeComments: true,
179+
skip,
180+
});
181+
},
182+
isSpaceBetweenTokens(first, second) {
183+
if (nodesOrTokensOverlap(first, second)) {
184+
return false;
185+
}
186+
187+
const [startingNodeOrToken, endingNodeOrToken] =
188+
first.range[1] <= second.range[0]
189+
? [first, second]
190+
: [second, first];
191+
const firstToken =
192+
sourceCode.getLastToken(startingNodeOrToken) ||
193+
startingNodeOrToken;
194+
const finalToken =
195+
sourceCode.getFirstToken(endingNodeOrToken) ||
196+
endingNodeOrToken;
197+
let currentToken = firstToken;
198+
199+
while (currentToken !== finalToken) {
200+
const nextToken = sourceCode.getTokenAfter(
201+
currentToken,
202+
{
203+
includeComments: true,
204+
},
205+
);
206+
207+
if (
208+
currentToken.range[1] !== nextToken.range[0] ||
209+
(nextToken !== finalToken &&
210+
nextToken.type === "JSXText" &&
211+
/\s/u.test(nextToken.value))
212+
) {
213+
return true;
214+
}
215+
216+
currentToken = nextToken;
217+
}
218+
219+
return false;
220+
},
221+
getJSDocComment(node) {
222+
let parent = node.parent;
223+
224+
switch (node.type) {
225+
case "ClassDeclaration":
226+
case "FunctionDeclaration":
227+
return findJSDocComment(
228+
looksLikeExport(parent) ? parent : node,
229+
sourceCode,
230+
);
231+
232+
case "ClassExpression":
233+
return findJSDocComment(parent.parent, sourceCode);
234+
235+
case "ArrowFunctionExpression":
236+
case "FunctionExpression":
237+
if (
238+
parent.type !== "CallExpression" &&
239+
parent.type !== "NewExpression"
240+
) {
241+
while (
242+
!sourceCode.getCommentsBefore(parent)
243+
.length &&
244+
!/Function/u.test(parent.type) &&
245+
parent.type !== "MethodDefinition" &&
246+
parent.type !== "Property"
247+
) {
248+
parent = parent.parent;
249+
250+
if (!parent) {
251+
break;
252+
}
253+
}
254+
255+
if (
256+
parent &&
257+
parent.type !== "FunctionDeclaration" &&
258+
parent.type !== "Program"
259+
) {
260+
return findJSDocComment(parent, sourceCode);
261+
}
262+
}
263+
264+
return findJSDocComment(node, sourceCode);
265+
266+
default:
267+
return null;
268+
}
269+
},
270+
});
271+
272+
Object.freeze(compatSourceCode);
273+
}
108274

109-
const newContext = Object.assign(Object.create(context), {
110-
parserServices: sourceCode.parserServices,
275+
let currentNode = compatSourceCode.ast;
276+
277+
const compatContext = Object.assign(Object.create(context), {
278+
parserServices: compatSourceCode.parserServices,
111279

112280
/*
113281
* The following methods rely on the current node in the traversal,
114282
* so we need to add them manually.
115283
*/
116284
getScope() {
117-
return sourceCode.getScope(currentNode);
285+
return compatSourceCode.getScope(currentNode);
118286
},
119287

120288
getAncestors() {
121-
return sourceCode.getAncestors(currentNode);
289+
return compatSourceCode.getAncestors(currentNode);
122290
},
123291

124292
markVariableAsUsed(variable) {
125-
sourceCode.markVariableAsUsed(variable, currentNode);
293+
compatSourceCode.markVariableAsUsed(variable, currentNode);
126294
},
127295
});
128296

297+
if (eslintVersion >= 10) {
298+
Object.assign(compatContext, {
299+
parserOptions: compatContext.languageOptions.parserOptions,
300+
301+
getCwd() {
302+
return compatContext.cwd;
303+
},
304+
305+
getFilename() {
306+
return compatContext.filename;
307+
},
308+
309+
getPhysicalFilename() {
310+
return compatContext.physicalFilename;
311+
},
312+
313+
getSourceCode() {
314+
return compatSourceCode;
315+
},
316+
});
317+
318+
Object.defineProperty(compatContext, "sourceCode", {
319+
enumerable: true,
320+
value: compatSourceCode,
321+
});
322+
}
323+
129324
// add passthrough methods
130325
for (const [
131326
contextMethodName,
132327
sourceCodeMethodName,
133328
] of removedMethodNames) {
134-
newContext[contextMethodName] =
135-
sourceCode[sourceCodeMethodName].bind(sourceCode);
329+
compatContext[contextMethodName] =
330+
compatSourceCode[sourceCodeMethodName].bind(compatSourceCode);
136331
}
137332

138333
// freeze just like the original context
139-
Object.freeze(newContext);
334+
Object.freeze(compatContext);
140335

141336
/*
142337
* Create the visitor object using the original create() method.
143338
* This is necessary to ensure that the visitor object is created
144339
* with the correct context.
145340
*/
146-
const visitor = originalCreate(newContext);
341+
const visitor = originalCreate(compatContext);
147342

148343
/*
149344
* Wrap each method in the visitor object to update the currentNode
@@ -184,7 +379,7 @@ export function fixupRule(ruleDefinition) {
184379
};
185380

186381
// copy `schema` property of function-style rule or top-level `schema` property of object-style rule into `meta` object
187-
// @ts-ignore -- top-level `schema` property was not offically supported for object-style rules so it doesn't exist in types
382+
// @ts-ignore -- top-level `schema` property was not officially supported for object-style rules so it doesn't exist in types
188383
const { schema } = ruleDefinition;
189384
if (schema) {
190385
if (!newRuleDefinition.meta) {
@@ -207,7 +402,7 @@ export function fixupRule(ruleDefinition) {
207402

208403
/**
209404
* Takes the given plugin and creates a new plugin with all of the rules wrapped
210-
* to provide the missing methods on the `context` object.
405+
* to provide missing methods on the `context` and `sourceCode` objects.
211406
* @param {FixupPluginDefinition} plugin The plugin to fix up.
212407
* @returns {FixupPluginDefinition} The fixed-up plugin.
213408
*/
@@ -244,7 +439,7 @@ export function fixupPluginRules(plugin) {
244439

245440
/**
246441
* Takes the given configuration and creates a new configuration with all of the
247-
* rules wrapped to provide the missing methods on the `context` object.
442+
* rules wrapped to provide missing methods on the `context` and `sourceCode` objects.
248443
* @param {FixupConfigArray|FixupConfig} config The configuration to fix up.
249444
* @returns {FixupConfigArray} The fixed-up configuration.
250445
*/

0 commit comments

Comments
 (0)