@medplum/eslint-config 5.1.17 → 5.1.21
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/index.mjs
CHANGED
|
@@ -7,13 +7,21 @@ import importPlugin from 'eslint-plugin-import';
|
|
|
7
7
|
import jsdoc from 'eslint-plugin-jsdoc';
|
|
8
8
|
import noOnlyTestsPlugin from 'eslint-plugin-no-only-tests';
|
|
9
9
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
10
|
-
import
|
|
10
|
+
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
|
11
|
+
// eslint-disable-next-line import/no-unresolved -- Node resolves this package export; eslint-plugin-import does not.
|
|
11
12
|
import tseslint from 'typescript-eslint';
|
|
13
|
+
import noTransactionCallbackInvokingRepo from './rules/no-transaction-callback-invoking-repo.mjs';
|
|
12
14
|
|
|
13
15
|
// Workaround for eslint-plugin-header ESLint 9 compatibility issue.
|
|
14
16
|
// See: https://github.com/Stuk/eslint-plugin-header/issues/57#issuecomment-2378485611
|
|
15
17
|
headerPlugin.rules.header.meta.schema = false;
|
|
16
18
|
|
|
19
|
+
const medplumPlugin = {
|
|
20
|
+
rules: {
|
|
21
|
+
'no-transaction-callback-invoking-repo': noTransactionCallbackInvokingRepo,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Core config applies to all source files.
|
|
19
27
|
* TypeScript-specific rules are in the tsConfig below.
|
|
@@ -147,7 +155,7 @@ export const tsConfig = {
|
|
|
147
155
|
},
|
|
148
156
|
tseslint.configs.strict,
|
|
149
157
|
reactHooks.configs.flat.recommended,
|
|
150
|
-
|
|
158
|
+
reactRefreshPlugin.configs.recommended,
|
|
151
159
|
],
|
|
152
160
|
plugins: {
|
|
153
161
|
'no-only-tests': noOnlyTestsPlugin,
|
|
@@ -295,13 +303,23 @@ export const medplumEslintConfig = [
|
|
|
295
303
|
},
|
|
296
304
|
coreConfig,
|
|
297
305
|
tsConfig,
|
|
306
|
+
{
|
|
307
|
+
files: ['packages/fhir-router/**/*.ts', 'packages/server/**/*.ts'],
|
|
308
|
+
plugins: {
|
|
309
|
+
medplum: medplumPlugin,
|
|
310
|
+
},
|
|
311
|
+
rules: {
|
|
312
|
+
'medplum/no-transaction-callback-invoking-repo': 'error',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
298
315
|
/**
|
|
299
|
-
* vite.config.ts
|
|
316
|
+
* vite.config.ts and vitest.config.ts are often outside package tsconfig include (e.g. rootDir src);
|
|
317
|
+
* skip type-aware parsing.
|
|
300
318
|
*
|
|
301
|
-
* we don't need type checking for vite.config.ts files
|
|
319
|
+
* we don't need type checking for vite.config.ts or vitest.config.ts files
|
|
302
320
|
*/
|
|
303
321
|
{
|
|
304
|
-
files: ['**/vite.config.ts'],
|
|
322
|
+
files: ['**/vite.config.ts', '**/vitest.config.ts'],
|
|
305
323
|
extends: [tseslint.configs.disableTypeChecked],
|
|
306
324
|
},
|
|
307
325
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medplum/eslint-config",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.21",
|
|
4
4
|
"description": "Shared ESLint configuration for Medplum projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -19,15 +19,20 @@
|
|
|
19
19
|
"author": "Medplum <hello@medplum.com>",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "index.mjs",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"lint:fix": "eslint . --fix",
|
|
25
|
+
"test": "node --test rules/*.test.mjs"
|
|
26
|
+
},
|
|
22
27
|
"devDependencies": {
|
|
23
28
|
"eslint": "9.39.4",
|
|
24
29
|
"eslint-plugin-header": "3.1.1",
|
|
25
30
|
"eslint-plugin-import": "2.32.0",
|
|
26
|
-
"eslint-plugin-jsdoc": "63.0.
|
|
31
|
+
"eslint-plugin-jsdoc": "63.0.2",
|
|
27
32
|
"eslint-plugin-no-only-tests": "3.4.0",
|
|
28
33
|
"eslint-plugin-react-hooks": "7.1.1",
|
|
29
34
|
"eslint-plugin-react-refresh": "0.5.2",
|
|
30
|
-
"typescript-eslint": "8.
|
|
35
|
+
"typescript-eslint": "8.61.0"
|
|
31
36
|
},
|
|
32
37
|
"peerDependencies": {
|
|
33
38
|
"eslint": "^9",
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flags uses of the repository that a `withTransaction`/`ensureInTransaction` callback was
|
|
6
|
+
* invoked on from inside that callback, including aliasing it (`const parent = repo`).
|
|
7
|
+
* Callback code must use the transaction-scoped repository passed as the callback parameter;
|
|
8
|
+
* the invoking repository is locked for the duration of the transaction and throws at runtime.
|
|
9
|
+
*
|
|
10
|
+
* This rule is fast feedback for the common cases, not the enforcement mechanism. The
|
|
11
|
+
* runtime scope guard in RepositoryConnection.assertScope() is authoritative. Known blind
|
|
12
|
+
* spots, deliberately left uncovered because closing them requires transitive variable
|
|
13
|
+
* resolution that is not worth the complexity:
|
|
14
|
+
* - Aliases created before the callback: `const r = repo; repo.withTransaction(() => r.foo())`
|
|
15
|
+
* - Reassignment through a mutable binding: `let r; r = repo;`
|
|
16
|
+
* - The repo escaping through a function argument, property, or field, e.g. the
|
|
17
|
+
* `this.repo` swap in BatchProcessor.withRepo() in fhir-router's batch.ts
|
|
18
|
+
* - When the invoking repo is reached through `this`, the rule stops descending at
|
|
19
|
+
* constructs that rebind `this` (non-arrow functions, classes), so a computed class
|
|
20
|
+
* member key that evaluates with the outer `this` is missed
|
|
21
|
+
*/
|
|
22
|
+
const transactionMethodNames = new Set(['withTransaction', 'ensureInTransaction']);
|
|
23
|
+
const thisRebindingNodeTypes = new Set([
|
|
24
|
+
'ClassDeclaration',
|
|
25
|
+
'ClassExpression',
|
|
26
|
+
'FunctionDeclaration',
|
|
27
|
+
'FunctionExpression',
|
|
28
|
+
'StaticBlock',
|
|
29
|
+
]);
|
|
30
|
+
const ignoredTraversalKeys = new Set([
|
|
31
|
+
'comments',
|
|
32
|
+
'decorators',
|
|
33
|
+
'loc',
|
|
34
|
+
'parent',
|
|
35
|
+
'range',
|
|
36
|
+
'returnType',
|
|
37
|
+
'tokens',
|
|
38
|
+
'typeAnnotation',
|
|
39
|
+
'typeArguments',
|
|
40
|
+
'typeParameters',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param context - ESLint rule context.
|
|
45
|
+
* @returns ESLint rule listener.
|
|
46
|
+
*/
|
|
47
|
+
function create(context) {
|
|
48
|
+
const sourceCode = context.sourceCode;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param node - The AST node.
|
|
52
|
+
* @returns The unwrapped expression.
|
|
53
|
+
*/
|
|
54
|
+
function unwrapExpression(node) {
|
|
55
|
+
let result = node;
|
|
56
|
+
while (
|
|
57
|
+
result?.type === 'ChainExpression' ||
|
|
58
|
+
result?.type === 'TSAsExpression' ||
|
|
59
|
+
result?.type === 'TSNonNullExpression' ||
|
|
60
|
+
result?.type === 'TSTypeAssertion'
|
|
61
|
+
) {
|
|
62
|
+
result = result.expression;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param node - The AST node.
|
|
69
|
+
* @returns The scope for the AST node.
|
|
70
|
+
*/
|
|
71
|
+
function getScope(node) {
|
|
72
|
+
return sourceCode.getScope(node);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param node - The identifier node.
|
|
77
|
+
* @returns The variable represented by the identifier.
|
|
78
|
+
*/
|
|
79
|
+
function findVariable(node) {
|
|
80
|
+
let scope = getScope(node);
|
|
81
|
+
while (scope) {
|
|
82
|
+
const variable = scope.set.get(node.name);
|
|
83
|
+
if (variable) {
|
|
84
|
+
return variable;
|
|
85
|
+
}
|
|
86
|
+
scope = scope.upper;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param node - The identifier node.
|
|
93
|
+
* @returns True if the identifier is an expression reference.
|
|
94
|
+
*/
|
|
95
|
+
function isReferenceIdentifier(node) {
|
|
96
|
+
const parent = node.parent;
|
|
97
|
+
if (!parent) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
switch (parent.type) {
|
|
102
|
+
case 'ArrayPattern':
|
|
103
|
+
case 'ImportDefaultSpecifier':
|
|
104
|
+
case 'ImportNamespaceSpecifier':
|
|
105
|
+
case 'LabeledStatement':
|
|
106
|
+
case 'MetaProperty':
|
|
107
|
+
case 'RestElement':
|
|
108
|
+
case 'TSTypeReference':
|
|
109
|
+
return false;
|
|
110
|
+
case 'VariableDeclarator':
|
|
111
|
+
// The declared name is not a reference, but the initializer is:
|
|
112
|
+
// `const alias = repo` reads the repo to create an alias for it
|
|
113
|
+
return parent.init === node;
|
|
114
|
+
case 'AssignmentPattern':
|
|
115
|
+
return parent.right === node;
|
|
116
|
+
case 'ClassDeclaration':
|
|
117
|
+
case 'ClassExpression':
|
|
118
|
+
case 'FunctionDeclaration':
|
|
119
|
+
case 'FunctionExpression':
|
|
120
|
+
return parent.id !== node;
|
|
121
|
+
case 'ImportSpecifier':
|
|
122
|
+
return parent.local !== node;
|
|
123
|
+
case 'MemberExpression':
|
|
124
|
+
return parent.object === node || parent.computed;
|
|
125
|
+
case 'MethodDefinition':
|
|
126
|
+
case 'Property':
|
|
127
|
+
case 'PropertyDefinition':
|
|
128
|
+
return parent.value === node || parent.computed;
|
|
129
|
+
default:
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param node - The member expression.
|
|
136
|
+
* @returns The static property name.
|
|
137
|
+
*/
|
|
138
|
+
function getStaticPropertyName(node) {
|
|
139
|
+
if (node.computed) {
|
|
140
|
+
const property = unwrapExpression(node.property);
|
|
141
|
+
return property?.type === 'Literal' && typeof property.value === 'string' ? property.value : undefined;
|
|
142
|
+
}
|
|
143
|
+
if (node.property.type === 'Identifier' || node.property.type === 'PrivateIdentifier') {
|
|
144
|
+
return node.property.name;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param expected - The expected member expression.
|
|
151
|
+
* @param candidate - The candidate member expression.
|
|
152
|
+
* @returns True if the member expressions use the same property.
|
|
153
|
+
*/
|
|
154
|
+
function hasSameProperty(expected, candidate) {
|
|
155
|
+
if (expected.computed || candidate.computed) {
|
|
156
|
+
return (
|
|
157
|
+
expected.computed === candidate.computed &&
|
|
158
|
+
sourceCode.getText(expected.property) === sourceCode.getText(candidate.property)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return getStaticPropertyName(expected) === getStaticPropertyName(candidate);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param expected - The expected expression.
|
|
166
|
+
* @param candidate - The candidate expression.
|
|
167
|
+
* @returns True if both expressions refer to the same expression.
|
|
168
|
+
*/
|
|
169
|
+
function isSameExpression(expected, candidate) {
|
|
170
|
+
const unwrappedExpected = unwrapExpression(expected);
|
|
171
|
+
const unwrappedCandidate = unwrapExpression(candidate);
|
|
172
|
+
if (!unwrappedExpected || !unwrappedCandidate || unwrappedExpected.type !== unwrappedCandidate.type) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
switch (unwrappedExpected.type) {
|
|
177
|
+
case 'Identifier':
|
|
178
|
+
return (
|
|
179
|
+
isReferenceIdentifier(unwrappedCandidate) &&
|
|
180
|
+
unwrappedExpected.name === unwrappedCandidate.name &&
|
|
181
|
+
findVariable(unwrappedExpected) === findVariable(unwrappedCandidate)
|
|
182
|
+
);
|
|
183
|
+
case 'ThisExpression':
|
|
184
|
+
return true;
|
|
185
|
+
case 'Super':
|
|
186
|
+
return true;
|
|
187
|
+
case 'MemberExpression':
|
|
188
|
+
return (
|
|
189
|
+
hasSameProperty(unwrappedExpected, unwrappedCandidate) &&
|
|
190
|
+
isSameExpression(unwrappedExpected.object, unwrappedCandidate.object)
|
|
191
|
+
);
|
|
192
|
+
default:
|
|
193
|
+
return sourceCode.getText(unwrappedExpected) === sourceCode.getText(unwrappedCandidate);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param node - The AST node.
|
|
199
|
+
* @returns True if the node is a function expression.
|
|
200
|
+
*/
|
|
201
|
+
function isCallbackFunction(node) {
|
|
202
|
+
return node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param node - The AST node.
|
|
207
|
+
* @param callback - The visitor callback.
|
|
208
|
+
* @param skipThisRebinding - Whether to stop descending at nodes that rebind `this`.
|
|
209
|
+
*/
|
|
210
|
+
function visit(node, callback, skipThisRebinding = false) {
|
|
211
|
+
if (callback(node)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (skipThisRebinding && thisRebindingNodeTypes.has(node.type)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const [key, value] of Object.entries(node)) {
|
|
219
|
+
if (ignoredTraversalKeys.has(key)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (Array.isArray(value)) {
|
|
223
|
+
for (const child of value) {
|
|
224
|
+
if (child?.type) {
|
|
225
|
+
visit(child, callback, skipThisRebinding);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else if (value?.type) {
|
|
229
|
+
visit(value, callback, skipThisRebinding);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param node - The AST node.
|
|
236
|
+
* @returns True if the expression contains a `this` or `super` reference.
|
|
237
|
+
*/
|
|
238
|
+
function usesThisBinding(node) {
|
|
239
|
+
let found = false;
|
|
240
|
+
visit(node, (candidate) => {
|
|
241
|
+
if (candidate.type === 'ThisExpression' || candidate.type === 'Super') {
|
|
242
|
+
found = true;
|
|
243
|
+
}
|
|
244
|
+
return found;
|
|
245
|
+
});
|
|
246
|
+
return found;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
CallExpression(node) {
|
|
251
|
+
const callee = unwrapExpression(node.callee);
|
|
252
|
+
if (callee?.type !== 'MemberExpression') {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const methodName = getStaticPropertyName(callee);
|
|
257
|
+
if (!methodName || !transactionMethodNames.has(methodName)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const invokingRepo = unwrapExpression(callee.object);
|
|
262
|
+
const callback = unwrapExpression(node.arguments[0]);
|
|
263
|
+
if (!invokingRepo || !callback || !isCallbackFunction(callback)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const repoUsesThis = usesThisBinding(invokingRepo);
|
|
268
|
+
if (repoUsesThis && callback.type === 'FunctionExpression') {
|
|
269
|
+
// A non-arrow callback rebinds `this`, so the invoking repo is unreachable inside it
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
visit(
|
|
274
|
+
callback.body,
|
|
275
|
+
(candidate) => {
|
|
276
|
+
if (!isSameExpression(invokingRepo, candidate)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
context.report({
|
|
281
|
+
node: candidate,
|
|
282
|
+
messageId: 'useCallbackRepo',
|
|
283
|
+
data: {
|
|
284
|
+
methodName,
|
|
285
|
+
repoName: sourceCode.getText(invokingRepo),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
return true;
|
|
289
|
+
},
|
|
290
|
+
repoUsesThis
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
297
|
+
const noTransactionCallbackInvokingRepoRule = {
|
|
298
|
+
meta: {
|
|
299
|
+
type: 'problem',
|
|
300
|
+
docs: {
|
|
301
|
+
description: 'require transaction callbacks to use the repository parameter instead of the invoking repository',
|
|
302
|
+
},
|
|
303
|
+
messages: {
|
|
304
|
+
useCallbackRepo: 'Use the repository passed to the {{methodName}} callback instead of `{{repoName}}`.',
|
|
305
|
+
},
|
|
306
|
+
schema: [],
|
|
307
|
+
},
|
|
308
|
+
create,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export default noTransactionCallbackInvokingRepoRule;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { RuleTester } from 'eslint';
|
|
5
|
+
import rule from './no-transaction-callback-invoking-repo.mjs';
|
|
6
|
+
|
|
7
|
+
const ruleTester = new RuleTester({
|
|
8
|
+
languageOptions: {
|
|
9
|
+
ecmaVersion: 'latest',
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-transaction-callback-invoking-repo', rule, {
|
|
15
|
+
valid: [
|
|
16
|
+
{
|
|
17
|
+
code: 'repo.withTransaction(async (txRepo) => txRepo.createResource(resource));',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
code: 'repo.ensureInTransaction(async (txRepo) => txRepo.createResource(resource));',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: 'ctx.repo.withTransaction(async (txRepo) => txRepo.createResource(resource));',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: 'this.repo.withTransaction(async (txRepo) => this.withRepo(txRepo, () => this.processBatch()));',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: 'repo.withTransaction(async (repo) => repo.createResource(resource));',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
code: 'repo.withTransaction(txFn);',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: [
|
|
36
|
+
'repo.withTransaction(async (txRepo) =>',
|
|
37
|
+
' txRepo.withTransaction(async (nestedRepo) => nestedRepo.createResource(resource))',
|
|
38
|
+
');',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
// Declaring a shadowing variable from the callback repo is fine; only the
|
|
43
|
+
// initializer of a declaration counts as a reference, not the declared name
|
|
44
|
+
code: [
|
|
45
|
+
'repo.withTransaction(async (txRepo) => {',
|
|
46
|
+
' const repo = txRepo;',
|
|
47
|
+
' return repo.createResource(resource);',
|
|
48
|
+
'});',
|
|
49
|
+
].join('\n'),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
// `this` inside a nested non-arrow function is a different binding
|
|
53
|
+
code: [
|
|
54
|
+
'this.repo.withTransaction(async (txRepo) => {',
|
|
55
|
+
' emitter.on("done", function () {',
|
|
56
|
+
' this.repo.createResource(resource);',
|
|
57
|
+
' });',
|
|
58
|
+
'});',
|
|
59
|
+
].join('\n'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
// `this` inside a nested class method is a different binding
|
|
63
|
+
code: [
|
|
64
|
+
'this.repo.withTransaction(async (txRepo) => {',
|
|
65
|
+
' class Helper {',
|
|
66
|
+
' run() {',
|
|
67
|
+
' return this.repo.createResource(resource);',
|
|
68
|
+
' }',
|
|
69
|
+
' }',
|
|
70
|
+
' return new Helper().run();',
|
|
71
|
+
'});',
|
|
72
|
+
].join('\n'),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
// A non-arrow callback rebinds `this`, so the invoking repo is unreachable inside it
|
|
76
|
+
code: 'this.repo.withTransaction(async function (txRepo) { return this.repo.createResource(resource); });',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
invalid: [
|
|
80
|
+
{
|
|
81
|
+
code: 'repo.withTransaction(async (txRepo) => repo.createResource(resource));',
|
|
82
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
code: 'repo.ensureInTransaction(async (txRepo) => repo.createResource(resource));',
|
|
86
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
code: 'ctx.repo.withTransaction(async (txRepo) => ctx.repo.createResource(resource));',
|
|
90
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
code: 'this.repo.withTransaction(async (txRepo) => this.repo.createResource(resource));',
|
|
94
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
code: [
|
|
98
|
+
'repo.withTransaction(async (txRepo) =>',
|
|
99
|
+
' repo.withTransaction(async (nestedRepo) => nestedRepo.createResource(resource))',
|
|
100
|
+
');',
|
|
101
|
+
].join('\n'),
|
|
102
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
// Aliasing the invoking repo inside the callback is flagged at the alias declaration
|
|
106
|
+
code: [
|
|
107
|
+
'repo.withTransaction(async (txRepo) => {',
|
|
108
|
+
' const parent = repo;',
|
|
109
|
+
' return parent.createResource(resource);',
|
|
110
|
+
'});',
|
|
111
|
+
].join('\n'),
|
|
112
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
code: [
|
|
116
|
+
'ctx.repo.withTransaction(async (txRepo) => {',
|
|
117
|
+
' const parent = ctx.repo;',
|
|
118
|
+
' return parent.createResource(resource);',
|
|
119
|
+
'});',
|
|
120
|
+
].join('\n'),
|
|
121
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
// Arrow functions inherit `this`, so the invoking repo is still reachable
|
|
125
|
+
code: [
|
|
126
|
+
'this.repo.withTransaction(async (txRepo) => {',
|
|
127
|
+
' const run = () => this.repo.createResource(resource);',
|
|
128
|
+
' return run();',
|
|
129
|
+
'});',
|
|
130
|
+
].join('\n'),
|
|
131
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
// Identifier resolution is unaffected by `this` rebinding in nested functions
|
|
135
|
+
code: [
|
|
136
|
+
'repo.withTransaction(async (txRepo) => {',
|
|
137
|
+
' function helper() {',
|
|
138
|
+
' return repo.createResource(resource);',
|
|
139
|
+
' }',
|
|
140
|
+
' return helper();',
|
|
141
|
+
'});',
|
|
142
|
+
].join('\n'),
|
|
143
|
+
errors: [{ messageId: 'useCallbackRepo' }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|