From 34bea560748f9392c585c3013262e51ee3cfdbc4 Mon Sep 17 00:00:00 2001 From: Hemant Jha Date: Thu, 5 Jun 2025 20:19:45 +0530 Subject: [PATCH] fix/issue-26 --- lib/rules/commands.ts | 79 +---------- lib/rules/commands/commandInId.ts | 50 +++++++ lib/rules/commands/commandInName.ts | 51 +++++++ lib/rules/commands/hotkeys.ts | 44 ++++++ lib/rules/commands/pluginId.ts | 47 +++++++ lib/rules/commands/pluginName.ts | 51 +++++++ lib/rules/settingTab/noGeneralHeading.ts | 93 +++++++++++++ lib/rules/settingTab/noHtmlHeaderElements.ts | 54 ++++++++ lib/rules/settingTab/noPluginNameInHeading.ts | 99 ++++++++++++++ lib/rules/settingTab/noSettingsInHeadings.ts | 102 ++++++++++++++ lib/rules/settingsTab.ts | 126 +----------------- 11 files changed, 604 insertions(+), 192 deletions(-) create mode 100644 lib/rules/commands/commandInId.ts create mode 100644 lib/rules/commands/commandInName.ts create mode 100644 lib/rules/commands/hotkeys.ts create mode 100644 lib/rules/commands/pluginId.ts create mode 100644 lib/rules/commands/pluginName.ts create mode 100644 lib/rules/settingTab/noGeneralHeading.ts create mode 100644 lib/rules/settingTab/noHtmlHeaderElements.ts create mode 100644 lib/rules/settingTab/noPluginNameInHeading.ts create mode 100644 lib/rules/settingTab/noSettingsInHeadings.ts diff --git a/lib/rules/commands.ts b/lib/rules/commands.ts index dac27bc..cef541d 100644 --- a/lib/rules/commands.ts +++ b/lib/rules/commands.ts @@ -1,73 +1,8 @@ -import {manifest} from "../readManifest"; +// This file has been split into separate rules in the commands/ directory: +// - commandInId.ts +// - commandInName.ts +// - hotkeys.ts +// - pluginId.ts +// - pluginName.ts -export = { - name: 'commands', - meta: { - docs: { - description: 'Command guidelines', - url: 'https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands' - }, - type: 'problem', - messages: { - hotkeys: 'We recommend against providing a default hotkey when possible.', - commandInId: 'Adding `command` to the command ID is not necessary.', - commandInName: 'Adding `command` to the command name is not necessary.', - pluginName: 'The command name should not include the plugin name.', - pluginId: 'The command ID should not include the plugin ID.' - }, - schema: [], - }, - defaultOptions: [], - create(context) { - return { - CallExpression(node) { - if (node.callee.type === 'MemberExpression' && - node.callee.property.name === 'addCommand' && - node.arguments.length > 0 && - node.arguments[0].type === 'ObjectExpression') { - - const argument = node.arguments[0]; - - argument.properties.forEach(property => { - if (property.key.type === 'Identifier') { - if (property.key.name === 'id' && property.value.type === 'Literal') { - if (property.value.value.toLowerCase().includes('command')) { - context.report({ - node: property, - messageId: 'commandInId' - }); - } - if (property.value.value.includes(manifest.id)) { - context.report({ - node: property, - messageId: 'pluginId' - }) - } - } - if (property.key.name === 'name' && property.value.type === 'Literal') { - if (property.value.value.toLowerCase().includes('command')) { - context.report({ - node: property, - messageId: 'commandInName' - }); - } - if (property.value.value.toLowerCase().includes(manifest.name.toLowerCase())) { - context.report({ - node: property, - messageId: 'pluginName' - }) - } - } - if (property.key.name === 'hotkeys') { - context.report({ - node: property, - messageId: 'hotkeys' - }); - } - } - }); - } - }, - }; - } -}; +export = {}; diff --git a/lib/rules/commands/commandInId.ts b/lib/rules/commands/commandInId.ts new file mode 100644 index 0000000..c19d661 --- /dev/null +++ b/lib/rules/commands/commandInId.ts @@ -0,0 +1,50 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "commandInId", + meta: { + docs: { + description: "Discourage adding `command` to the command ID.", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands", + }, + type: "problem", + messages: { + commandInId: "Adding `command` to the command ID is not necessary.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.name === "addCommand" && + node.arguments.length > 0 && + node.arguments[0].type === "ObjectExpression" + ) { + const argument = node.arguments[0]; + + argument.properties.forEach((property) => { + if ( + property.key.type === "Identifier" && + property.key.name === "id" && + property.value.type === "Literal" + ) { + if ( + property.value.value + .toLowerCase() + .includes("command") + ) { + context.report({ + node: property, + messageId: "commandInId", + }); + } + } + }); + } + }, + }; + }, +}; diff --git a/lib/rules/commands/commandInName.ts b/lib/rules/commands/commandInName.ts new file mode 100644 index 0000000..f213eda --- /dev/null +++ b/lib/rules/commands/commandInName.ts @@ -0,0 +1,51 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "commandInName", + meta: { + docs: { + description: "Discourage adding `command` to the command name.", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands", + }, + type: "problem", + messages: { + commandInName: + "Adding `command` to the command name is not necessary.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.name === "addCommand" && + node.arguments.length > 0 && + node.arguments[0].type === "ObjectExpression" + ) { + const argument = node.arguments[0]; + + argument.properties.forEach((property) => { + if ( + property.key.type === "Identifier" && + property.key.name === "name" && + property.value.type === "Literal" + ) { + if ( + property.value.value + .toLowerCase() + .includes("command") + ) { + context.report({ + node: property, + messageId: "commandInName", + }); + } + } + }); + } + }, + }; + }, +}; diff --git a/lib/rules/commands/hotkeys.ts b/lib/rules/commands/hotkeys.ts new file mode 100644 index 0000000..3986707 --- /dev/null +++ b/lib/rules/commands/hotkeys.ts @@ -0,0 +1,44 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "hotkeys", + meta: { + docs: { + description: "Discourage providing default hotkeys.", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands", + }, + type: "problem", + messages: { + hotkeys: + "We recommend against providing a default hotkey when possible.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.name === "addCommand" && + node.arguments.length > 0 && + node.arguments[0].type === "ObjectExpression" + ) { + const argument = node.arguments[0]; + + argument.properties.forEach((property) => { + if ( + property.key.type === "Identifier" && + property.key.name === "hotkeys" + ) { + context.report({ + node: property, + messageId: "hotkeys", + }); + } + }); + } + }, + }; + }, +}; diff --git a/lib/rules/commands/pluginId.ts b/lib/rules/commands/pluginId.ts new file mode 100644 index 0000000..b488236 --- /dev/null +++ b/lib/rules/commands/pluginId.ts @@ -0,0 +1,47 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "pluginId", + meta: { + docs: { + description: + "Discourage including the plugin ID in the command ID.", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands", + }, + type: "problem", + messages: { + pluginId: "The command ID should not include the plugin ID.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.name === "addCommand" && + node.arguments.length > 0 && + node.arguments[0].type === "ObjectExpression" + ) { + const argument = node.arguments[0]; + + argument.properties.forEach((property) => { + if ( + property.key.type === "Identifier" && + property.key.name === "id" && + property.value.type === "Literal" + ) { + if (property.value.value.includes(manifest.id)) { + context.report({ + node: property, + messageId: "pluginId", + }); + } + } + }); + } + }, + }; + }, +}; diff --git a/lib/rules/commands/pluginName.ts b/lib/rules/commands/pluginName.ts new file mode 100644 index 0000000..5ec19f4 --- /dev/null +++ b/lib/rules/commands/pluginName.ts @@ -0,0 +1,51 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "pluginName", + meta: { + docs: { + description: + "Discourage including the plugin name in the command name.", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Commands", + }, + type: "problem", + messages: { + pluginName: "The command name should not include the plugin name.", + }, + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.property.name === "addCommand" && + node.arguments.length > 0 && + node.arguments[0].type === "ObjectExpression" + ) { + const argument = node.arguments[0]; + + argument.properties.forEach((property) => { + if ( + property.key.type === "Identifier" && + property.key.name === "name" && + property.value.type === "Literal" + ) { + if ( + property.value.value + .toLowerCase() + .includes(manifest.name.toLowerCase()) + ) { + context.report({ + node: property, + messageId: "pluginName", + }); + } + } + }); + } + }, + }; + }, +}; diff --git a/lib/rules/settingTab/noGeneralHeading.ts b/lib/rules/settingTab/noGeneralHeading.ts new file mode 100644 index 0000000..5ec96d9 --- /dev/null +++ b/lib/rules/settingTab/noGeneralHeading.ts @@ -0,0 +1,93 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "no-general-heading", + meta: { + docs: { + description: 'Don\'t use a "general" heading in your settings', + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#UI+text", + }, + type: "problem", + messages: { + general: 'Don\'t use a "general" heading in your settings', + }, + schema: [], + fixable: "code", + }, + defaultOptions: [], + create(context) { + let insidePluginSettingTab = false; + return { + ClassDeclaration(node) { + if ( + node.superClass && + node.superClass.name === "PluginSettingTab" + ) { + insidePluginSettingTab = true; + } + }, + "ClassDeclaration:exit"(node) { + if (insidePluginSettingTab) { + insidePluginSettingTab = false; + } + }, + ExpressionStatement(node) { + if ( + insidePluginSettingTab && + node.expression?.type === "CallExpression" + ) { + const methods = []; + let callExpr = node.expression; + let text = ""; + + while ( + callExpr && + callExpr.type === "CallExpression" && + callExpr.callee && + callExpr.callee.property + ) { + const property = callExpr.callee.property; + + if (property && property.type === "Identifier") { + const callName = property.name; + const args = callExpr.arguments; + + if ( + callName === "setName" && + args.length > 0 && + args[0]?.type === "Literal" + ) { + methods.push(callName); + text = args[0].value; + } + + if (callName === "setHeading") { + methods.push(callName); + } + + callExpr = callExpr.callee.object; + } else { + break; + } + + if ( + callExpr?.type === "NewExpression" && + callExpr.callee?.type === "Identifier" && + callExpr.callee.name === "Setting" && + methods.includes("setName") && + methods.includes("setHeading") + ) { + if (text.toLowerCase().includes("general")) { + context.report({ + node, + messageId: "general", + fix: (fixer) => fixer.remove(node), + }); + } + } + } + } + }, + }; + }, +}; diff --git a/lib/rules/settingTab/noHtmlHeaderElements.ts b/lib/rules/settingTab/noHtmlHeaderElements.ts new file mode 100644 index 0000000..4ee1e2c --- /dev/null +++ b/lib/rules/settingTab/noHtmlHeaderElements.ts @@ -0,0 +1,54 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "no-html-header-elements", + meta: { + docs: { + description: "Don't use HTML header elements for settings headings", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#UI+text", + }, + type: "problem", + messages: { + headingEl: "Don't use HTML header elements for settings headings", + }, + schema: [], + fixable: "code", + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if (node.callee.type === "MemberExpression") { + if (node.callee.property.name === "createEl") { + const args = node.arguments; + + const containerObjectName = node.callee.object.name; + const textProperty = node.arguments[1].properties.find( + (property) => + property.key.value === "text" || + property.key.name === "text" + ); + if (!textProperty) return; + const textValue = textProperty.value.value; + + if ( + ["h1", "h2", "h3", "h4", "h5", "h6"].includes( + args[0].value + ) + ) { + context.report({ + node: args[0], + messageId: "headingEl", + fix: (fixer) => + fixer.replaceText( + node, + `new Setting(${containerObjectName}).setName("${textValue}").setHeading()` + ), + }); + } + } + } + }, + }; + }, +}; diff --git a/lib/rules/settingTab/noPluginNameInHeading.ts b/lib/rules/settingTab/noPluginNameInHeading.ts new file mode 100644 index 0000000..0512495 --- /dev/null +++ b/lib/rules/settingTab/noPluginNameInHeading.ts @@ -0,0 +1,99 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "no-plugin-name-in-heading", + meta: { + docs: { + description: + "Don't include a heading with the plugin name in settings", + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#UI+text", + }, + type: "problem", + messages: { + pluginName: + "Don't include a heading with the plugin name in settings", + }, + schema: [], + fixable: "code", + }, + defaultOptions: [], + create(context) { + let insidePluginSettingTab = false; + return { + ClassDeclaration(node) { + if ( + node.superClass && + node.superClass.name === "PluginSettingTab" + ) { + insidePluginSettingTab = true; + } + }, + "ClassDeclaration:exit"(node) { + if (insidePluginSettingTab) { + insidePluginSettingTab = false; + } + }, + ExpressionStatement(node) { + if ( + insidePluginSettingTab && + node.expression?.type === "CallExpression" + ) { + const methods = []; + let callExpr = node.expression; + let text = ""; + + while ( + callExpr && + callExpr.type === "CallExpression" && + callExpr.callee && + callExpr.callee.property + ) { + const property = callExpr.callee.property; + + if (property && property.type === "Identifier") { + const callName = property.name; + const args = callExpr.arguments; + + if ( + callName === "setName" && + args.length > 0 && + args[0]?.type === "Literal" + ) { + methods.push(callName); + text = args[0].value; + } + + if (callName === "setHeading") { + methods.push(callName); + } + + callExpr = callExpr.callee.object; + } else { + break; + } + + if ( + callExpr?.type === "NewExpression" && + callExpr.callee?.type === "Identifier" && + callExpr.callee.name === "Setting" && + methods.includes("setName") && + methods.includes("setHeading") + ) { + if ( + text + .toLowerCase() + .includes(manifest.name.toLowerCase()) + ) { + context.report({ + node, + messageId: "pluginName", + fix: (fixer) => fixer.remove(node), + }); + } + } + } + } + }, + }; + }, +}; diff --git a/lib/rules/settingTab/noSettingsInHeadings.ts b/lib/rules/settingTab/noSettingsInHeadings.ts new file mode 100644 index 0000000..7d8e155 --- /dev/null +++ b/lib/rules/settingTab/noSettingsInHeadings.ts @@ -0,0 +1,102 @@ +import { manifest } from "../../readManifest"; + +export = { + name: "no-settings-in-headings", + meta: { + docs: { + description: 'Avoid "settings" in settings headings', + url: "https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#UI+text", + }, + type: "problem", + messages: { + settings: 'Avoid "settings" in settings headings', + }, + schema: [], + fixable: "code", + }, + defaultOptions: [], + create(context) { + let insidePluginSettingTab = false; + return { + ClassDeclaration(node) { + if ( + node.superClass && + node.superClass.name === "PluginSettingTab" + ) { + insidePluginSettingTab = true; + } + }, + "ClassDeclaration:exit"(node) { + if (insidePluginSettingTab) { + insidePluginSettingTab = false; + } + }, + ExpressionStatement(node) { + if ( + insidePluginSettingTab && + node.expression?.type === "CallExpression" + ) { + const methods = []; + let callExpr = node.expression; + let text = ""; + + while ( + callExpr && + callExpr.type === "CallExpression" && + callExpr.callee && + callExpr.callee.property + ) { + const property = callExpr.callee.property; + + if (property && property.type === "Identifier") { + const callName = property.name; + const args = callExpr.arguments; + + if ( + callName === "setName" && + args.length > 0 && + args[0]?.type === "Literal" + ) { + methods.push(callName); + text = args[0].value; + } + + if (callName === "setHeading") { + methods.push(callName); + } + + callExpr = callExpr.callee.object; + } else { + break; + } + + if ( + callExpr?.type === "NewExpression" && + callExpr.callee?.type === "Identifier" && + callExpr.callee.name === "Setting" && + methods.includes("setName") && + methods.includes("setHeading") + ) { + if ( + [ + "settings", + "options", + "configuration", + "config", + ].some((str) => + text.toLowerCase().includes(str) + ) + ) { + context.report({ + node, + messageId: "settings", + fix: (fixer) => fixer.remove(node), + }); + } + } + } + } + }, + }; + }, +}; diff --git a/lib/rules/settingsTab.ts b/lib/rules/settingsTab.ts index 464ab7d..251979c 100644 --- a/lib/rules/settingsTab.ts +++ b/lib/rules/settingsTab.ts @@ -1,121 +1,7 @@ -import {manifest} from "../readManifest"; - -export = { - name: 'settings-tab', - meta: { - docs: { - description: '', - url: 'https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#UI+text' - }, - type: 'problem', - messages: { - settings: 'Avoid "settings" in settings headings', - general: 'Don\'t use a "general" heading in your settings', - pluginName: 'Don\'t include a heading with the plugin name in settings', - headingEl: 'Don\'t use HTML header elements for settings headings', - }, - schema: [], - fixable: 'code' - }, - defaultOptions: [], - create(context) { - let insidePluginSettingTab = false; - return { - ClassDeclaration(node) { - // Check if the class extends `PluginSettingTab` - if(node.superClass && node.superClass.name === 'PluginSettingTab') { - insidePluginSettingTab = true; - } - }, - 'ClassDeclaration:exit'(node) { - if(insidePluginSettingTab) { - insidePluginSettingTab = false; - } - }, - - - ExpressionStatement(node) { - if (insidePluginSettingTab && node.expression?.type === 'CallExpression') { - const methods = []; - let callExpr = node.expression; - let text = ''; - - while (callExpr && callExpr.type === 'CallExpression' && callExpr.callee && callExpr.callee.property) { - const property = callExpr.callee.property; - - if (property && property.type === 'Identifier') { - const property = callExpr.callee.property; - - if (property.type === 'Identifier') { - const callName = property.name; - const args = callExpr.arguments; - - if (callName === 'setName' && args.length > 0 && args[0]?.type === 'Literal') { - methods.push(callName); - text = args[0].value; - } - - if (callName === 'setHeading') { - methods.push(callName); - } - } - - callExpr = callExpr.callee.object; - } - - if (callExpr?.type === 'NewExpression' && - callExpr.callee?.type === 'Identifier' && - callExpr.callee.name === 'Setting' && - methods.includes('setName') && - methods.includes('setHeading')) { - - if(['settings', 'options', 'configuration', 'config'].some(str => text.toLowerCase().includes(str))) { - context.report({ - node, - messageId: 'settings', - fix: fixer => fixer.remove(node), - }) - } - if(text.toLowerCase().includes('general')) { - context.report({ - node, - messageId: 'general', - fix: fixer => fixer.remove(node), - }) - } - if(text.toLowerCase().includes(manifest.name.toLowerCase())) { - context.report({ - node, - messageId: 'pluginName', - fix: fixer => fixer.remove(node), - }) - } - } - } - } - }, - - CallExpression(node) { - if (node.callee.type === 'MemberExpression') { - if (node.callee.property.name === 'createEl') { - const args = node.arguments; - - const containerObjectName = node.callee.object.name; - const textProperty = node.arguments[1].properties.find(property => property.key.value === 'text' || property.key.name === 'text'); - if (!textProperty) return; - const textValue = textProperty.value.value; - - if(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(args[0].value)) { - context.report({ - node: args[0], - messageId: 'headingEl', - fix: fixer => fixer.replaceText(node, `new Setting(${containerObjectName}).setName("${textValue}").setHeading()`) - }); - } - } - } - } - }; - } -}; +// This file has been split into separate rules in the settingsTab/ directory: +// - noSettingsInHeadings.ts +// - noGeneralHeading.ts +// - noPluginNameInHeading.ts +// - noHtmlHeaderElements.ts +export = {};