-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for namespaces and fix imports
1 parent
b4fc9c4
commit d4af3bd
Showing
3 changed files
with
314 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 117 additions & 61 deletions
178
packages/eslint-plugin-mobx/src/missing-make-observable.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,127 @@ | ||
'use strict'; | ||
"use strict" | ||
|
||
const { findAncestor, isMobxDecorator } = require('./utils.js'); | ||
const { findAncestor, isMobxDecorator } = require("./utils.js") | ||
|
||
function create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
const sourceCode = context.getSourceCode() | ||
|
||
return { | ||
'Decorator': decorator => { | ||
if (!isMobxDecorator(decorator)) return; | ||
const clazz = findAncestor(decorator, node => node.type === 'ClassDeclaration' || node.type === 'ClassExpression'); | ||
if (!clazz) return; | ||
// ClassDeclaration > ClassBody > [] | ||
const constructor = clazz.body.body.find(node => node.kind === 'constructor' && node.value.type === 'FunctionExpression') ?? | ||
clazz.body.body.find(node => node.kind === 'constructor'); | ||
// MethodDefinition > FunctionExpression > BlockStatement > [] | ||
const isMakeObservable = node => node.expression?.callee?.name === 'makeObservable' && node.expression?.arguments[0]?.type === 'ThisExpression'; | ||
const makeObservable = constructor?.value.body?.body.find(isMakeObservable)?.expression; | ||
let namespaceImportName = undefined // is 'mobxFoo' when import * as mobxFoo from 'mobx' | ||
let makeObserverImportName = undefined // is 'mobxFoo' when import * as mobxFoo from 'mobx' | ||
let lastSpecifierImport = undefined | ||
|
||
if (makeObservable) { | ||
// make sure second arg is nullish | ||
const secondArg = makeObservable.arguments[1]; | ||
if (secondArg && secondArg.value !== null && secondArg.name !== 'undefined') { | ||
context.report({ | ||
node: makeObservable, | ||
messageId: 'secondArgMustBeNullish', | ||
}) | ||
} | ||
} else { | ||
const fix = fixer => { | ||
if (constructor?.value.type === 'TSEmptyBodyFunctionExpression') { | ||
// constructor() - yes this a thing | ||
const closingBracket = sourceCode.getLastToken(constructor.value); | ||
return fixer.insertTextAfter(closingBracket, ' { makeObservable(this); }') | ||
} else if (constructor) { | ||
// constructor() {} | ||
const closingBracket = sourceCode.getLastToken(constructor.value.body); | ||
return fixer.insertTextBefore(closingBracket, ';makeObservable(this);') | ||
} else { | ||
// class C {} | ||
const openingBracket = sourceCode.getFirstToken(clazz.body); | ||
return fixer.insertTextAfter(openingBracket, '\nconstructor() { makeObservable(this); }') | ||
} | ||
}; | ||
return { | ||
ImportDeclaration: node => { | ||
if (node.source.value !== "mobx") return | ||
|
||
context.report({ | ||
node: clazz, | ||
messageId: 'missingMakeObservable', | ||
fix, | ||
}) | ||
} | ||
}, | ||
}; | ||
// Collect the imports | ||
|
||
for (const specifier of node.specifiers) { | ||
if (specifier.type === "ImportNamespaceSpecifier") { | ||
namespaceImportName = specifier.local.name | ||
} | ||
|
||
if (specifier.type === "ImportSpecifier") { | ||
lastSpecifierImport = specifier | ||
if (specifier.imported.name === "makeObservable") { | ||
makeObserverImportName = specifier.local.name | ||
} | ||
} | ||
} | ||
}, | ||
Decorator: decorator => { | ||
if (!isMobxDecorator(decorator, namespaceImportName)) return | ||
const clazz = findAncestor( | ||
decorator, | ||
node => node.type === "ClassDeclaration" || node.type === "ClassExpression" | ||
) | ||
if (!clazz) return | ||
// ClassDeclaration > ClassBody > [] | ||
const constructor = | ||
clazz.body.body.find( | ||
node => node.kind === "constructor" && node.value.type === "FunctionExpression" | ||
) ?? clazz.body.body.find(node => node.kind === "constructor") | ||
// MethodDefinition > FunctionExpression > BlockStatement > [] | ||
const isMakeObservable = node => | ||
node.expression?.callee?.name === "makeObservable" && | ||
node.expression?.arguments[0]?.type === "ThisExpression" | ||
const makeObservable = constructor?.value.body?.body.find(isMakeObservable)?.expression | ||
|
||
if (makeObservable) { | ||
// make sure second arg is nullish | ||
const secondArg = makeObservable.arguments[1] | ||
if (secondArg && secondArg.value !== null && secondArg.name !== "undefined") { | ||
context.report({ | ||
node: makeObservable, | ||
messageId: "secondArgMustBeNullish" | ||
}) | ||
} | ||
} else { | ||
const fix = fixer => { | ||
const fixes = [] | ||
let makeObservableExpr = "makeObservable" | ||
|
||
// Insert the makeObservable import if required | ||
if (!namespaceImportName && !makeObserverImportName && lastSpecifierImport) { | ||
fixes.push(fixer.insertTextAfter(lastSpecifierImport, ", makeObservable")) | ||
} else if (namespaceImportName) { | ||
makeObservableExpr = `${namespaceImportName}.makeObservable` | ||
} else if (makeObserverImportName) { | ||
makeObservableExpr = makeObserverImportName | ||
} | ||
|
||
if (constructor?.value.type === "TSEmptyBodyFunctionExpression") { | ||
// constructor() - yes this a thing | ||
const closingBracket = sourceCode.getLastToken(constructor.value) | ||
fixes.push( | ||
fixer.insertTextAfter( | ||
closingBracket, | ||
` { ${makeObservableExpr}(this); }` | ||
) | ||
) | ||
} else if (constructor) { | ||
// constructor() {} | ||
const closingBracket = sourceCode.getLastToken(constructor.value.body) | ||
fixes.push( | ||
fixer.insertTextBefore(closingBracket, `;${makeObservableExpr}(this);`) | ||
) | ||
} else { | ||
// class C {} | ||
const openingBracket = sourceCode.getFirstToken(clazz.body) | ||
fixes.push( | ||
fixer.insertTextAfter( | ||
openingBracket, | ||
`\nconstructor() { ${makeObservableExpr}(this); }` | ||
) | ||
) | ||
} | ||
|
||
return fixes | ||
} | ||
|
||
context.report({ | ||
node: clazz, | ||
messageId: "missingMakeObservable", | ||
fix | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
fixable: 'code', | ||
docs: { | ||
description: 'prevents missing `makeObservable(this)` when using decorators', | ||
recommended: true, | ||
suggestion: false, | ||
}, | ||
messages: { | ||
missingMakeObservable: "Constructor is missing `makeObservable(this)`.", | ||
secondArgMustBeNullish: "`makeObservable`'s second argument must be nullish or not provided when using decorators." | ||
meta: { | ||
type: "problem", | ||
fixable: "code", | ||
docs: { | ||
description: "prevents missing `makeObservable(this)` when using decorators", | ||
recommended: true, | ||
suggestion: false | ||
}, | ||
messages: { | ||
missingMakeObservable: "Constructor is missing `makeObservable(this)`.", | ||
secondArgMustBeNullish: | ||
"`makeObservable`'s second argument must be nullish or not provided when using decorators." | ||
} | ||
}, | ||
}, | ||
create, | ||
}; | ||
create | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,52 @@ | ||
'use strict'; | ||
"use strict" | ||
|
||
const mobxDecorators = new Set(['observable', 'computed', 'action', 'flow', 'override']); | ||
const mobxDecorators = new Set(["observable", "computed", "action", "flow", "override"]) | ||
|
||
function isMobxDecorator(decorator) { | ||
return mobxDecorators.has(decorator.expression.name) // @foo | ||
|| mobxDecorators.has(decorator.expression.callee?.name) // @foo() | ||
|| mobxDecorators.has(decorator.expression.object?.name) // @foo.bar | ||
function isMobxDecorator(decorator, namespace) { | ||
if (namespace !== undefined) { | ||
let memberExpression | ||
if (decorator.expression.type === "MemberExpression") { | ||
memberExpression = decorator.expression | ||
} | ||
|
||
if ( | ||
decorator.expression.type === "CallExpression" && | ||
decorator.expression.callee.type === "MemberExpression" | ||
) { | ||
memberExpression = decorator.expression.callee | ||
} | ||
|
||
if ( | ||
memberExpression.object.name === namespace || | ||
memberExpression.object.object?.name === namespace | ||
) { | ||
return true | ||
} | ||
} | ||
|
||
return ( | ||
mobxDecorators.has(decorator.expression.name) || // @foo | ||
mobxDecorators.has(decorator.expression.callee?.name) || // @foo() | ||
mobxDecorators.has(decorator.expression.object?.name) | ||
) // @foo.bar | ||
} | ||
|
||
function findAncestor(node, match) { | ||
const { parent } = node; | ||
if (!parent) return; | ||
if (match(parent)) return parent; | ||
return findAncestor(parent, match); | ||
const { parent } = node | ||
if (!parent) return | ||
if (match(parent)) return parent | ||
return findAncestor(parent, match) | ||
} | ||
|
||
function assert(expr, error) { | ||
if (!expr) { | ||
error ??= 'Assertion failed'; | ||
error = error instanceof Error ? error : new Error(error) | ||
throw error; | ||
} | ||
if (!expr) { | ||
error ??= "Assertion failed" | ||
error = error instanceof Error ? error : new Error(error) | ||
throw error | ||
} | ||
} | ||
|
||
module.exports = { | ||
findAncestor, | ||
isMobxDecorator, | ||
} | ||
findAncestor, | ||
isMobxDecorator | ||
} |