@lexical/eslint-plugin 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LexicalEslintPlugin.d.ts +21 -0
- package/LexicalEslintPlugin.dev.js +699 -0
- package/LexicalEslintPlugin.dev.mjs +697 -0
- package/LexicalEslintPlugin.js +11 -0
- package/LexicalEslintPlugin.js.flow +11 -0
- package/LexicalEslintPlugin.mjs +12 -0
- package/LexicalEslintPlugin.node.mjs +10 -0
- package/LexicalEslintPlugin.prod.js +20 -0
- package/LexicalEslintPlugin.prod.mjs +9 -0
- package/README.md +216 -0
- package/index.d.ts +14 -0
- package/package.json +49 -0
- package/rules/rules-of-lexical.d.ts +52 -0
- package/util/buildMatcher.d.ts +6 -0
- package/util/getFunctionName.d.ts +1 -0
- package/util/getParentAssignmentName.d.ts +1 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
function _mergeNamespaces(n, m) {
|
|
12
|
+
for (var i = 0; i < m.length; i++) {
|
|
13
|
+
var e = m[i];
|
|
14
|
+
if (typeof e !== 'string' && !Array.isArray(e)) { for (var k in e) {
|
|
15
|
+
if (k !== 'default' && !(k in n)) {
|
|
16
|
+
n[k] = e[k];
|
|
17
|
+
}
|
|
18
|
+
} }
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getDefaultExportFromCjs (x) {
|
|
24
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var name$1 = "@lexical/eslint-plugin";
|
|
28
|
+
var description = "Lexical specific linting rules for ESLint";
|
|
29
|
+
var keywords = [
|
|
30
|
+
"eslint",
|
|
31
|
+
"eslint-plugin",
|
|
32
|
+
"eslintplugin",
|
|
33
|
+
"lexical",
|
|
34
|
+
"editor"
|
|
35
|
+
];
|
|
36
|
+
var version$1 = "0.15.0";
|
|
37
|
+
var license = "MIT";
|
|
38
|
+
var repository = {
|
|
39
|
+
type: "git",
|
|
40
|
+
url: "git+https://github.com/facebook/lexical.git",
|
|
41
|
+
directory: "packages/lexical-eslint-plugin"
|
|
42
|
+
};
|
|
43
|
+
var main = "LexicalEslintPlugin.js";
|
|
44
|
+
var types = "index.d.ts";
|
|
45
|
+
var bugs = {
|
|
46
|
+
url: "https://github.com/facebook/lexical/issues"
|
|
47
|
+
};
|
|
48
|
+
var homepage = "https://lexical.dev/docs/packages/lexical-eslint-plugin";
|
|
49
|
+
var sideEffects = false;
|
|
50
|
+
var peerDependencies = {
|
|
51
|
+
eslint: ">=7.31.0 || ^8.0.0"
|
|
52
|
+
};
|
|
53
|
+
var exports$1 = {
|
|
54
|
+
".": {
|
|
55
|
+
"import": {
|
|
56
|
+
types: "./index.d.ts",
|
|
57
|
+
development: "./LexicalEslintPlugin.dev.mjs",
|
|
58
|
+
production: "./LexicalEslintPlugin.prod.mjs",
|
|
59
|
+
node: "./LexicalEslintPlugin.node.mjs",
|
|
60
|
+
"default": "./LexicalEslintPlugin.mjs"
|
|
61
|
+
},
|
|
62
|
+
require: {
|
|
63
|
+
types: "./index.d.ts",
|
|
64
|
+
development: "./LexicalEslintPlugin.dev.js",
|
|
65
|
+
production: "./LexicalEslintPlugin.prod.js",
|
|
66
|
+
"default": "./LexicalEslintPlugin.js"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var devDependencies = {
|
|
71
|
+
"@types/eslint": "^8.56.9"
|
|
72
|
+
};
|
|
73
|
+
var module$1 = "LexicalEslintPlugin.mjs";
|
|
74
|
+
var require$$0 = {
|
|
75
|
+
name: name$1,
|
|
76
|
+
description: description,
|
|
77
|
+
keywords: keywords,
|
|
78
|
+
version: version$1,
|
|
79
|
+
license: license,
|
|
80
|
+
repository: repository,
|
|
81
|
+
main: main,
|
|
82
|
+
types: types,
|
|
83
|
+
bugs: bugs,
|
|
84
|
+
homepage: homepage,
|
|
85
|
+
sideEffects: sideEffects,
|
|
86
|
+
peerDependencies: peerDependencies,
|
|
87
|
+
exports: exports$1,
|
|
88
|
+
devDependencies: devDependencies,
|
|
89
|
+
module: module$1
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
var rulesOfLexical$1 = {};
|
|
93
|
+
|
|
94
|
+
var getFunctionName$1 = {};
|
|
95
|
+
|
|
96
|
+
var getParentAssignmentName$2 = {};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
100
|
+
*
|
|
101
|
+
* This source code is licensed under the MIT license found in the
|
|
102
|
+
* LICENSE file in the root directory of this source tree.
|
|
103
|
+
*
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the static name of an AST node's parent, used to determine the name of an
|
|
108
|
+
* anonymous function declaration, possibly through a higher order function call.
|
|
109
|
+
* This was extracted from the body of getFunctionName so it could also be used
|
|
110
|
+
* in the context of `useCallback` or `useMemo`, e.g.
|
|
111
|
+
* `const $fun = useCallback(() => {}, [])` where the name is not the direct
|
|
112
|
+
* parent of the anonymous function.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('eslint').Rule.Node} node
|
|
115
|
+
*/
|
|
116
|
+
getParentAssignmentName$2.getParentAssignmentName = function getParentAssignmentName(node) {
|
|
117
|
+
// Unlike React's rules of hooks, this does not check property assignment.
|
|
118
|
+
// The rules of lexical $function convention only applies to functions,
|
|
119
|
+
// not methods or properties.
|
|
120
|
+
const parentNode = node.parent;
|
|
121
|
+
if (parentNode.type === 'VariableDeclarator' && parentNode.init === node) {
|
|
122
|
+
// const $function = () => {};
|
|
123
|
+
return parentNode.id;
|
|
124
|
+
} else if (parentNode.type === 'AssignmentExpression' && parentNode.right === node && parentNode.operator === '=') {
|
|
125
|
+
// $function = () => {};
|
|
126
|
+
return parentNode.left;
|
|
127
|
+
} else {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
134
|
+
*
|
|
135
|
+
* This source code is licensed under the MIT license found in the
|
|
136
|
+
* LICENSE file in the root directory of this source tree.
|
|
137
|
+
*
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
// @ts-check
|
|
141
|
+
|
|
142
|
+
const {
|
|
143
|
+
getParentAssignmentName: getParentAssignmentName$1
|
|
144
|
+
} = getParentAssignmentName$2;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Gets the static name of a function AST node. For function declarations it is
|
|
148
|
+
* easy. For anonymous function expressions it is much harder. If you search for
|
|
149
|
+
* `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
|
|
150
|
+
* where JS gives anonymous function expressions names. We roughly detect the
|
|
151
|
+
* same AST nodes with some exceptions to better fit our use case.
|
|
152
|
+
*
|
|
153
|
+
* @param {import('eslint').Rule.Node} node
|
|
154
|
+
*/
|
|
155
|
+
getFunctionName$1.getFunctionName = function getFunctionName(node) {
|
|
156
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' && node.id) {
|
|
157
|
+
// function $function() {}
|
|
158
|
+
// const whatever = function $function() {};
|
|
159
|
+
//
|
|
160
|
+
// Function declaration or function expression names win over any
|
|
161
|
+
// assignment statements or other renames.
|
|
162
|
+
return node.id;
|
|
163
|
+
} else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
164
|
+
// This checks for assignments such as
|
|
165
|
+
// const $function = function () {};
|
|
166
|
+
// const $function = () => {};
|
|
167
|
+
return getParentAssignmentName$1(node);
|
|
168
|
+
} else {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
var buildMatcher$1 = {};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
177
|
+
*
|
|
178
|
+
* This source code is licensed under the MIT license found in the
|
|
179
|
+
* LICENSE file in the root directory of this source tree.
|
|
180
|
+
*
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @typedef {import('estree').Node} Node
|
|
185
|
+
* @typedef {import('estree').Identifier} Identifier
|
|
186
|
+
* @typedef {(name: string, node: Identifier) => boolean} NameIdentifierMatcher
|
|
187
|
+
* @typedef {NameIdentifierMatcher | string | RegExp | undefined} ToMatcher
|
|
188
|
+
* @typedef {(node: Identifier | undefined) => boolean} IdentifierMatcher
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Escape a string for exact match in a RegExp
|
|
193
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
|
|
194
|
+
*
|
|
195
|
+
* @param {string} string
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function escapeRegExp(string) {
|
|
199
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build an Identifier Node Matcher from the given ToMatcher arguments.
|
|
204
|
+
* The Matcher is roughly equivalent to building RegExp from all of the
|
|
205
|
+
* sources and or-ing them together. String arguments are treated as
|
|
206
|
+
* RegExp sources and will be escaped with an implicit '^...$' wrapper
|
|
207
|
+
* unless it starts with a '(' or '^'
|
|
208
|
+
*
|
|
209
|
+
* @param {(ToMatcher | ToMatcher[])[]} args
|
|
210
|
+
* @returns {IdentifierMatcher}
|
|
211
|
+
*/
|
|
212
|
+
buildMatcher$1.buildMatcher = function buildMatcher(...toMatchers) {
|
|
213
|
+
/** @type {Matcher[]} */
|
|
214
|
+
const matchFuns = [];
|
|
215
|
+
/** @type {string[]} */
|
|
216
|
+
const regExpSources = [];
|
|
217
|
+
for (const arg of toMatchers.flat(1)) {
|
|
218
|
+
if (!arg) {
|
|
219
|
+
continue;
|
|
220
|
+
} else if (typeof arg === 'string') {
|
|
221
|
+
regExpSources.push(/^[(^]/.test(arg) ? arg : `^${escapeRegExp(arg)}$`);
|
|
222
|
+
} else if (arg && arg instanceof RegExp) {
|
|
223
|
+
if (arg.flags) {
|
|
224
|
+
matchFuns.push(s => arg.test(s));
|
|
225
|
+
} else {
|
|
226
|
+
regExpSources.push(arg.source);
|
|
227
|
+
}
|
|
228
|
+
} else if (typeof arg === 'function') {
|
|
229
|
+
matchFuns.push(arg);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const pattern = regExpSources.map(s => `(?:${s})`).join('|');
|
|
233
|
+
if (pattern) {
|
|
234
|
+
const re = new RegExp(pattern);
|
|
235
|
+
matchFuns.push(s => re.test(s));
|
|
236
|
+
}
|
|
237
|
+
return node => {
|
|
238
|
+
if (node) {
|
|
239
|
+
if (node.type !== 'Identifier') {
|
|
240
|
+
// Runtime type invariant check
|
|
241
|
+
throw new Error(`Expecting Identifier, not ${node.type}`);
|
|
242
|
+
}
|
|
243
|
+
for (const matcher of matchFuns) {
|
|
244
|
+
if (matcher(node.name, node)) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
255
|
+
*
|
|
256
|
+
* This source code is licensed under the MIT license found in the
|
|
257
|
+
* LICENSE file in the root directory of this source tree.
|
|
258
|
+
*
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
// @ts-check
|
|
262
|
+
|
|
263
|
+
const {
|
|
264
|
+
getFunctionName
|
|
265
|
+
} = getFunctionName$1;
|
|
266
|
+
const {
|
|
267
|
+
getParentAssignmentName
|
|
268
|
+
} = getParentAssignmentName$2;
|
|
269
|
+
const {
|
|
270
|
+
buildMatcher
|
|
271
|
+
} = buildMatcher$1;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @typedef {import('eslint').Rule.NodeParentExtension} NodeParentExtension
|
|
275
|
+
* @typedef {import('estree').CallExpression & NodeParentExtension} CallExpression
|
|
276
|
+
* @typedef {import('estree').Identifier & NodeParentExtension} Identifier
|
|
277
|
+
* @typedef {import('eslint').Rule.RuleContext} RuleContext
|
|
278
|
+
* @typedef {import('eslint').Rule.Fix} Fix
|
|
279
|
+
* @typedef {import('eslint').Rule.Node} Node
|
|
280
|
+
* @typedef {import('eslint').Rule.RuleModule} RuleModule
|
|
281
|
+
* @typedef {import('eslint').Rule.ReportFixer} ReportFixer
|
|
282
|
+
* @typedef {import('eslint').SourceCode} SourceCode
|
|
283
|
+
* @typedef {import('eslint').Scope.Variable} Variable
|
|
284
|
+
* @typedef {import('eslint').Scope.Scope} Scope
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Find the variable associated with the given Identifier
|
|
289
|
+
*
|
|
290
|
+
* @param {SourceCode} sourceCode
|
|
291
|
+
* @param {Identifier} identifier
|
|
292
|
+
*/
|
|
293
|
+
function getIdentifierVariable(sourceCode, identifier) {
|
|
294
|
+
const scopeManager = sourceCode.scopeManager;
|
|
295
|
+
for (let node = /** @type {Node | null} */identifier; node; node = /** @type {Node | null}*/node.parent) {
|
|
296
|
+
const variable = scopeManager.getDeclaredVariables(node).find(v => v.identifiers.includes(identifier));
|
|
297
|
+
if (variable) {
|
|
298
|
+
return variable;
|
|
299
|
+
}
|
|
300
|
+
const scope = scopeManager.acquire(node);
|
|
301
|
+
if (scope) {
|
|
302
|
+
return scope.set.get(identifier.name) || (scope.upper ? scope.upper.set.get(identifier.name) : undefined);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @typedef {import('../util/buildMatcher.js').ToMatcher} ToMatcher
|
|
310
|
+
* @typedef {import('../util/buildMatcher.js').IdentifierMatcher} IdentifierMatcher
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @template T
|
|
315
|
+
* @typedef {Object} BaseMatchers<T>
|
|
316
|
+
* @property {T} isDollarFunction Catch all identifiers that begin with '$' or 'INTERNAL_$' followed by a lowercase Latin character or underscore
|
|
317
|
+
* @property {T} isIgnoredFunction These functions may call any $functions even though they do not have the isDollarFunction naming convention
|
|
318
|
+
* @property {T} isLexicalProvider Certain calls through the editor or editorState allow for implicit access to call $functions: read, registerCommand, registerNodeTransform, update.
|
|
319
|
+
* @property {T} isSafeDollarFunction It's usually safe to call $isNode functions, so any '$is' or 'INTERNAL_$is' function may be called in any context.
|
|
320
|
+
*/
|
|
321
|
+
|
|
322
|
+
/** @type {BaseMatchers<Exclude<ToMatcher, undefined>[]>} */
|
|
323
|
+
const BaseMatchers = {
|
|
324
|
+
isDollarFunction: [/^\$[a-z_]/],
|
|
325
|
+
isIgnoredFunction: [],
|
|
326
|
+
isLexicalProvider: ['parseEditorState', 'read', 'registerCommand', 'registerNodeTransform', 'update'],
|
|
327
|
+
isSafeDollarFunction: [/^\$is[A-Z_]/]
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @typedef {Partial<BaseMatchers<ToMatcher | ToMatcher[]>>} RulesOfLexicalOptions
|
|
332
|
+
* @typedef {BaseMatchers<IdentifierMatcher>} Matchers
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {RuleContext} context
|
|
337
|
+
* @returns {Matchers}
|
|
338
|
+
*/
|
|
339
|
+
function compileMatchers(context) {
|
|
340
|
+
const rval = /** @type {Matchers} */{};
|
|
341
|
+
for (const k_ in BaseMatchers) {
|
|
342
|
+
const k = /** @type {keyof Matchers} */k_;
|
|
343
|
+
rval[k] = buildMatcher(BaseMatchers[k], parseMatcherOption(context, k));
|
|
344
|
+
}
|
|
345
|
+
return rval;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Hook functions start with use followed by a capital latin letter.
|
|
350
|
+
*
|
|
351
|
+
* @param {Node | undefined} node
|
|
352
|
+
*/
|
|
353
|
+
function isHookFunctionIdentifier(node) {
|
|
354
|
+
return node && node.type === 'Identifier' && /^use([A-Z]|$)/.test(node.name);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Return this node if is an Identifier, otherwise if it is a MemberExpression such as
|
|
359
|
+
* `editor.read` return the Identifier of its property ('read' in this case).
|
|
360
|
+
*
|
|
361
|
+
* @param {Node | undefined} node
|
|
362
|
+
* @returns {Identifier | undefined}
|
|
363
|
+
*/
|
|
364
|
+
function getFunctionNameIdentifier(node) {
|
|
365
|
+
if (!node) {
|
|
366
|
+
return;
|
|
367
|
+
} else if (node.type === 'Identifier') {
|
|
368
|
+
return node;
|
|
369
|
+
} else if (node.type === 'MemberExpression' && !node.computed) {
|
|
370
|
+
return getFunctionNameIdentifier( /** @type {Node} */node.property);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get the function's name, or if it is defined with a hook
|
|
376
|
+
* (e.g. useMemo, useCallback), then get the name of the variable the result
|
|
377
|
+
* is assigned to.
|
|
378
|
+
*
|
|
379
|
+
* @param {Node} node
|
|
380
|
+
*/
|
|
381
|
+
function getLexicalFunctionName(node) {
|
|
382
|
+
const name = getFunctionName(node);
|
|
383
|
+
if (name) {
|
|
384
|
+
return name;
|
|
385
|
+
}
|
|
386
|
+
const nodeParent = node.parent;
|
|
387
|
+
if (nodeParent.type === 'CallExpression' && nodeParent.arguments[0] === node) {
|
|
388
|
+
const parentName = getFunctionNameIdentifier( /** @type {Node} */nodeParent.callee);
|
|
389
|
+
if (isHookFunctionIdentifier(parentName)) {
|
|
390
|
+
return getParentAssignmentName(nodeParent);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Return a name suitable for a suggestion.
|
|
397
|
+
* isDollarFunction(getFirstSuggestion(name)) should
|
|
398
|
+
* be true for any given name.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} name
|
|
401
|
+
* @returns {string}
|
|
402
|
+
*/
|
|
403
|
+
function getFirstSuggestion(name) {
|
|
404
|
+
if (/^[a-z]/.test(name)) {
|
|
405
|
+
return '$' + name;
|
|
406
|
+
} else if (/^[A-Z][a-z]/.test(name)) {
|
|
407
|
+
return '$' + name.slice(0, 1).toLowerCase() + name.slice(1);
|
|
408
|
+
} else {
|
|
409
|
+
return `$_${name}`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the suggested name for a variable and add an underscore if it shadows
|
|
415
|
+
* or conflicts with an existing variable.
|
|
416
|
+
*
|
|
417
|
+
* @param {Identifier} nameIdentifier
|
|
418
|
+
* @param {Variable | undefined} variable
|
|
419
|
+
*/
|
|
420
|
+
function getSuggestName(nameIdentifier, variable) {
|
|
421
|
+
const suggestName = getFirstSuggestion(nameIdentifier.name);
|
|
422
|
+
// Add an underscore if this would shadow an existing name
|
|
423
|
+
if (variable) {
|
|
424
|
+
for (let scope = /** @type {Scope | null} */variable.scope; scope; scope = scope.upper) {
|
|
425
|
+
if (scope.set.has(suggestName)) {
|
|
426
|
+
return suggestName + '_';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return suggestName;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get the export declaration for a variable, if it has one.
|
|
435
|
+
*
|
|
436
|
+
* @param {Variable | undefined} variable
|
|
437
|
+
*/
|
|
438
|
+
function getExportDeclaration(variable) {
|
|
439
|
+
if (variable && variable.defs.length === 1) {
|
|
440
|
+
const [{
|
|
441
|
+
node
|
|
442
|
+
}] = variable.defs;
|
|
443
|
+
if (node.parent.type === 'ExportNamedDeclaration') {
|
|
444
|
+
// export function foo();
|
|
445
|
+
return node.parent;
|
|
446
|
+
} else if (node.parent.type === 'VariableDeclaration' && node.parent.parent.type === 'ExportNamedDeclaration') {
|
|
447
|
+
// export const foo = () => {};
|
|
448
|
+
return node.parent.parent;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The comment we insert when an export is renamed.
|
|
455
|
+
*
|
|
456
|
+
* @param {Record<'caller'|'suggestName', string>} data
|
|
457
|
+
*/
|
|
458
|
+
function renameExportText({
|
|
459
|
+
caller,
|
|
460
|
+
suggestName
|
|
461
|
+
}) {
|
|
462
|
+
return `\n/** @deprecated renamed to {@link ${suggestName}} by @lexical/eslint-plugin rules-of-lexical */\nexport const ${caller} = ${suggestName};`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {RuleContext} context
|
|
467
|
+
* @param {string} optionName
|
|
468
|
+
* @returns {ToMatcher}
|
|
469
|
+
*/
|
|
470
|
+
function parseMatcherOption(context, optionName) {
|
|
471
|
+
const options = Array.isArray(context.options) ? context.options[0] : undefined;
|
|
472
|
+
return options && optionName in options ? options[optionName] : undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** @param {RuleContext} context */
|
|
476
|
+
function getSourceCode(context) {
|
|
477
|
+
// Deprecated in 8.x but we are still on 7.x
|
|
478
|
+
return context.getSourceCode();
|
|
479
|
+
}
|
|
480
|
+
const matcherSchema = {
|
|
481
|
+
oneOf: [{
|
|
482
|
+
type: 'string'
|
|
483
|
+
}, {
|
|
484
|
+
contains: {
|
|
485
|
+
type: 'string'
|
|
486
|
+
},
|
|
487
|
+
type: 'array'
|
|
488
|
+
}]
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/** @type {RuleModule} */
|
|
492
|
+
rulesOfLexical$1.rulesOfLexical = {
|
|
493
|
+
create(context) {
|
|
494
|
+
const sourceCode = getSourceCode(context);
|
|
495
|
+
const matchers = compileMatchers(context);
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* When this set is non-empty it means that we are visiting a node
|
|
499
|
+
* that should not be analyzed.
|
|
500
|
+
*
|
|
501
|
+
* @type {Set<Node>}
|
|
502
|
+
*/
|
|
503
|
+
const ignoreSet = new Set();
|
|
504
|
+
/**
|
|
505
|
+
* The set of Identifier nodes that have been reported, we do not
|
|
506
|
+
* want to report the same node more than once (a function making several
|
|
507
|
+
* calls to $functions only needs to be renamed once!)
|
|
508
|
+
*
|
|
509
|
+
* @type {Set<Identifier>}
|
|
510
|
+
*/
|
|
511
|
+
const reportedSet = new Set();
|
|
512
|
+
/**
|
|
513
|
+
* The current stack of functions
|
|
514
|
+
*
|
|
515
|
+
* @type {{ name?: Identifier, node: Node }[]} funStack
|
|
516
|
+
*/
|
|
517
|
+
const funStack = [];
|
|
518
|
+
const shouldIgnore = () => {
|
|
519
|
+
if (ignoreSet.size > 0) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
// Ignore property assignments
|
|
523
|
+
const lastFunction = funStack[funStack.length - 1];
|
|
524
|
+
return lastFunction && lastFunction.node.parent.type === 'Property';
|
|
525
|
+
};
|
|
526
|
+
const pushIgnoredNode = ( /** @type {Node} */node) => ignoreSet.add(node);
|
|
527
|
+
const popIgnoredNode = ( /** @type {Node} */node) => ignoreSet.delete(node);
|
|
528
|
+
const pushFunction = ( /** @type {Node} */node) => {
|
|
529
|
+
const name = getFunctionNameIdentifier(getLexicalFunctionName(node));
|
|
530
|
+
funStack.push({
|
|
531
|
+
name,
|
|
532
|
+
node
|
|
533
|
+
});
|
|
534
|
+
if (matchers.isDollarFunction(name) || matchers.isIgnoredFunction(name) || matchers.isLexicalProvider(name)) {
|
|
535
|
+
pushIgnoredNode(node);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const popFunction = ( /** @type {Node} */node) => {
|
|
539
|
+
funStack.pop();
|
|
540
|
+
popIgnoredNode(node);
|
|
541
|
+
};
|
|
542
|
+
const getParentLexicalFunctionNameIdentifier = ( /** @type {Node} */_node) => {
|
|
543
|
+
const pair = funStack[funStack.length - 1];
|
|
544
|
+
return pair ? pair.name : undefined;
|
|
545
|
+
};
|
|
546
|
+
// Find all $function calls that are not inside a class or inside a $function
|
|
547
|
+
// by visiting all function definitions and calls
|
|
548
|
+
return {
|
|
549
|
+
ArrowFunctionExpression: pushFunction,
|
|
550
|
+
'ArrowFunctionExpression:exit': popFunction,
|
|
551
|
+
CallExpression: node => {
|
|
552
|
+
if (shouldIgnore()) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const calleeName = getFunctionNameIdentifier( /** @type {Node} */node.callee);
|
|
556
|
+
if (matchers.isLexicalProvider(calleeName) || matchers.isSafeDollarFunction(calleeName)) {
|
|
557
|
+
pushIgnoredNode(node);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!matchers.isDollarFunction(calleeName)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const nameIdentifier = getParentLexicalFunctionNameIdentifier();
|
|
564
|
+
if (!nameIdentifier || reportedSet.has(nameIdentifier)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
reportedSet.add(nameIdentifier);
|
|
568
|
+
const variable = getIdentifierVariable(sourceCode, nameIdentifier);
|
|
569
|
+
const suggestName = getSuggestName(nameIdentifier, variable);
|
|
570
|
+
const exportDeclaration = getExportDeclaration(variable);
|
|
571
|
+
const data = {
|
|
572
|
+
callee: sourceCode.getText(node.callee),
|
|
573
|
+
caller: sourceCode.getText(nameIdentifier),
|
|
574
|
+
suggestName
|
|
575
|
+
};
|
|
576
|
+
/** @type {ReportFixer} */
|
|
577
|
+
const fix = fixer => {
|
|
578
|
+
/** @type {Set<Identifier>} */
|
|
579
|
+
const replaced = new Set();
|
|
580
|
+
/** @type {Fix[]} */
|
|
581
|
+
const fixes = [];
|
|
582
|
+
const renameIdentifier = ( /** @type {Identifier} */identifier) => {
|
|
583
|
+
if (!replaced.has(identifier)) {
|
|
584
|
+
replaced.add(identifier);
|
|
585
|
+
fixes.push(fixer.replaceText(identifier, suggestName));
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
renameIdentifier(nameIdentifier);
|
|
589
|
+
if (exportDeclaration) {
|
|
590
|
+
fixes.push(fixer.insertTextAfter(exportDeclaration, renameExportText(data)));
|
|
591
|
+
}
|
|
592
|
+
if (variable) {
|
|
593
|
+
for (const ref of variable.references) {
|
|
594
|
+
renameIdentifier( /** @type {Identifier} */ref.identifier);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return fixes;
|
|
598
|
+
};
|
|
599
|
+
context.report({
|
|
600
|
+
data,
|
|
601
|
+
fix,
|
|
602
|
+
messageId: 'rulesOfLexicalReport',
|
|
603
|
+
node: nameIdentifier,
|
|
604
|
+
suggest: [{
|
|
605
|
+
data,
|
|
606
|
+
fix,
|
|
607
|
+
messageId: 'rulesOfLexicalSuggestion'
|
|
608
|
+
}]
|
|
609
|
+
});
|
|
610
|
+
},
|
|
611
|
+
'CallExpression:exit': popIgnoredNode,
|
|
612
|
+
ClassBody: pushIgnoredNode,
|
|
613
|
+
'ClassBody:exit': popIgnoredNode,
|
|
614
|
+
FunctionDeclaration: pushFunction,
|
|
615
|
+
'FunctionDeclaration:exit': popFunction,
|
|
616
|
+
FunctionExpression: pushFunction,
|
|
617
|
+
'FunctionExpression:exit': popFunction
|
|
618
|
+
};
|
|
619
|
+
},
|
|
620
|
+
meta: {
|
|
621
|
+
docs: {
|
|
622
|
+
description: 'enforces the Rules of Lexical',
|
|
623
|
+
recommended: true,
|
|
624
|
+
url: 'https://lexical.dev/docs/packages/lexical-eslint-plugin'
|
|
625
|
+
},
|
|
626
|
+
fixable: 'code',
|
|
627
|
+
hasSuggestions: true,
|
|
628
|
+
messages: {
|
|
629
|
+
rulesOfLexicalReport: '{{ callee }} called from {{ caller }}, without $ prefix or read/update context',
|
|
630
|
+
rulesOfLexicalSuggestion: 'Rename {{ caller }} to {{ suggestName }}'
|
|
631
|
+
},
|
|
632
|
+
schema: [{
|
|
633
|
+
additionalProperties: false,
|
|
634
|
+
properties: {
|
|
635
|
+
isDollarFunction: matcherSchema,
|
|
636
|
+
isIgnoredFunction: matcherSchema,
|
|
637
|
+
isLexicalProvider: matcherSchema,
|
|
638
|
+
isSafeDollarFunction: matcherSchema
|
|
639
|
+
},
|
|
640
|
+
type: 'object'
|
|
641
|
+
}],
|
|
642
|
+
type: 'suggestion'
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
648
|
+
*
|
|
649
|
+
* This source code is licensed under the MIT license found in the
|
|
650
|
+
* LICENSE file in the root directory of this source tree.
|
|
651
|
+
*
|
|
652
|
+
*/
|
|
653
|
+
|
|
654
|
+
// @ts-check
|
|
655
|
+
|
|
656
|
+
const {
|
|
657
|
+
name,
|
|
658
|
+
version
|
|
659
|
+
} = require$$0;
|
|
660
|
+
const {
|
|
661
|
+
rulesOfLexical
|
|
662
|
+
} = rulesOfLexical$1;
|
|
663
|
+
const all = {
|
|
664
|
+
plugins: ['@lexical'],
|
|
665
|
+
rules: {
|
|
666
|
+
'@lexical/rules-of-lexical': 'warn'
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const plugin = {
|
|
670
|
+
configs: {
|
|
671
|
+
all,
|
|
672
|
+
recommended: all
|
|
673
|
+
},
|
|
674
|
+
meta: {
|
|
675
|
+
name,
|
|
676
|
+
version
|
|
677
|
+
},
|
|
678
|
+
rules: {
|
|
679
|
+
'rules-of-lexical': rulesOfLexical
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
var LexicalEslintPlugin = plugin;
|
|
683
|
+
|
|
684
|
+
var LexicalEslintPlugin$1 = /*@__PURE__*/getDefaultExportFromCjs(LexicalEslintPlugin);
|
|
685
|
+
|
|
686
|
+
var plugin$1 = /*#__PURE__*/_mergeNamespaces({
|
|
687
|
+
__proto__: null,
|
|
688
|
+
default: LexicalEslintPlugin$1
|
|
689
|
+
}, [LexicalEslintPlugin]);
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
693
|
+
*
|
|
694
|
+
* This source code is licensed under the MIT license found in the
|
|
695
|
+
* LICENSE file in the root directory of this source tree.
|
|
696
|
+
*
|
|
697
|
+
*/
|
|
698
|
+
|
|
699
|
+
module.exports = plugin$1;
|