UNPKG

eslint-plugin-obsidianmd

Version:

Validates guidelines for Obsidian plugins

107 lines (106 loc) 4.42 kB
import { ESLintUtils, } from "@typescript-eslint/utils"; const ruleCreator = ESLintUtils.RuleCreator((name) => `https://github.com/obsidianmd/eslint-plugin/blob/master/docs/rules/${name}.md`); // Check if a type is a subclass of a given class name. function isSubclassOf(type, className, services) { const constraint = type.getConstraint(); if (constraint) { type = constraint; } const symbol = type.getSymbol(); if (symbol?.name === className) { return true; } const baseTypes = type.getBaseTypes(); if (baseTypes) { for (const baseType of baseTypes) { if (isSubclassOf(baseType, className, services)) { return true; } } } return false; } export default ruleCreator({ name: "no-view-references-in-plugin", meta: { type: "problem", docs: { description: "Disallow storing references to custom views directly in the plugin, which can cause memory leaks.", }, schema: [], messages: { avoidViewReference: "Do not assign a view instance to a plugin property within `registerView`. This can cause memory leaks. Create and return the view directly.", }, }, defaultOptions: [], create(context) { const services = ESLintUtils.getParserServices(context); const sourceCode = context.sourceCode; // Checks if an expression is `this` or an alias initialized with `this`. const isThisOrThisAlias = (node) => { if (node.type === "ThisExpression") { return true; } if (node.type === "Identifier") { const scope = sourceCode.getScope(node); const reference = scope.references.find((ref) => ref.identifier === node); const variable = reference?.resolved; if (!variable?.defs[0]) { return false; } const defNode = variable.defs[0].node; // Add a type guard to ensure the definition node is a // VariableDeclarator before accessing its `init` property. if (defNode.type === "VariableDeclarator" && defNode.init?.type === "ThisExpression") { return true; } } return false; }; const checkForBadAssignment = (node) => { if (node?.type === "AssignmentExpression" && node.left.type === "MemberExpression" && isThisOrThisAlias(node.left.object) && node.right.type === "NewExpression") { const newInstanceType = services.getTypeAtLocation(node.right); if (isSubclassOf(newInstanceType, "View", services)) { context.report({ node: node, messageId: "avoidViewReference", }); } } }; return { "CallExpression[callee.property.name='registerView']"(callNode) { const callee = callNode.callee; if (callee.type !== "MemberExpression") return; const callerType = services.getTypeAtLocation(callee.object); if (!isSubclassOf(callerType, "Plugin", services)) return; const factory = callNode.arguments[1]; if (!factory || (factory.type !== "ArrowFunctionExpression" && factory.type !== "FunctionExpression")) { return; } const factoryBody = factory.body; if (factoryBody.type === "AssignmentExpression") { checkForBadAssignment(factoryBody); } else if (factoryBody.type === "BlockStatement") { for (const statement of factoryBody.body) { if (statement.type === "ExpressionStatement") { checkForBadAssignment(statement.expression); } else if (statement.type === "ReturnStatement") { checkForBadAssignment(statement.argument); } } } }, }; }, });