diff --git a/.eslintrc.json b/.eslintrc.json index 89f8227361..d43aa649c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,6 @@ { "extends": ["@excalidraw/eslint-config", "react-app"], + "plugins": ["excalidraw"], "rules": { "import/order": [ "warn", @@ -38,6 +39,7 @@ { "allowReferrer": true } - ] + ], + "excalidraw/no-binding-direct-mod": "error" } } diff --git a/package.json b/package.json index 9397d00400..c69e92c347 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dotenv": "16.0.1", "eslint-config-prettier": "8.5.0", "eslint-config-react-app": "7.0.1", + "eslint-plugin-eslint": "file:packages/eslint", "eslint-plugin-import": "2.31.0", "eslint-plugin-prettier": "3.3.1", "http-server": "14.1.1", diff --git a/packages/eslint/index.js b/packages/eslint/index.js new file mode 100644 index 0000000000..944360ab5a --- /dev/null +++ b/packages/eslint/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + "no-binding-direct-mod": require("./no-binding-direct-mod"), + }, +}; diff --git a/packages/eslint/no-binding-direct-mod.js b/packages/eslint/no-binding-direct-mod.js new file mode 100644 index 0000000000..b0e1fc5f91 --- /dev/null +++ b/packages/eslint/no-binding-direct-mod.js @@ -0,0 +1,84 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: "problem", + docs: { + description: + "disallow direct mutation of startBinding or endBinding via mutateElement", + category: "Best Practices", + recommended: false, + }, + fixable: null, + schema: [], + messages: { + noDirectBindingMutation: + "Direct mutation of {{ property }} via mutateElement() is not allowed. Use proper binding update functions instead.", + }, + }, + + create(context) { + return { + CallExpression(node) { + // Check if this is a call to mutateElement (direct call or method call) + let isMutateElementCall = false; + + if ( + node.callee.type === "Identifier" && + node.callee.name === "mutateElement" + ) { + // Direct call: mutateElement() + isMutateElementCall = true; + } else if ( + node.callee.type === "MemberExpression" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "mutateElement" + ) { + // Method call: something.mutateElement() or this.scene.mutateElement() + isMutateElementCall = true; + } + + if (isMutateElementCall) { + // mutateElement can have different argument patterns: + // 1. mutateElement(element, updates) - 2 args + // 2. mutateElement(element, elementsMap, updates) - 3 args + // 3. mutateElement(element, updates, options) - 3 args + let updatesArg = null; + + if (node.arguments.length >= 2) { + // Try second argument first (most common pattern) + const secondArg = node.arguments[1]; + if (secondArg.type === "ObjectExpression") { + updatesArg = secondArg; + } else if (node.arguments.length >= 3) { + // If second arg is not an object, try third argument + const thirdArg = node.arguments[2]; + if (thirdArg.type === "ObjectExpression") { + updatesArg = thirdArg; + } + } + } + + if (updatesArg) { + // Look for startBinding or endBinding properties + for (const property of updatesArg.properties) { + if ( + property.type === "Property" && + property.key.type === "Identifier" && + (property.key.name === "startBinding" || + property.key.name === "endBinding") + ) { + context.report({ + node: property, + messageId: "noDirectBindingMutation", + data: { + property: property.key.name, + }, + }); + } + } + } + } + }, + }; + }, +}; diff --git a/packages/eslint/package.json b/packages/eslint/package.json new file mode 100644 index 0000000000..6dfe485120 --- /dev/null +++ b/packages/eslint/package.json @@ -0,0 +1,11 @@ +{ + "name": "eslint-plugin-excalidraw", + "version": "0.1.0", + "main": "index.js", + "scripts": { + "lint": "eslint ." + }, + "devDependencies": { + "eslint": "^7.32.0" + } +} diff --git a/yarn.lock b/yarn.lock index 21374749a6..348fb63cfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5265,6 +5265,9 @@ eslint-module-utils@^2.12.0: dependencies: debug "^3.2.7" +"eslint-plugin-eslint@file:packages/eslint": + version "0.1.0" + eslint-plugin-flowtype@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912"