Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions config/custom-eslint-rules/enforce-icon-tooltip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { TSESLint, type TSESTree} from '@typescript-eslint/utils';

type MessageIds = 'missingTooltip' | 'emptyTooltip';

const EnforceVisynIconTooltip: TSESLint.RuleModule<MessageIds> = {
defaultOptions: [],
meta: {
type: 'problem',
messages: {
missingTooltip: 'Your icon component is missing a tooltip property.',
emptyTooltip: 'Your icon component has an empty tooltip property.',
},
fixable: 'code', // TODO
schema: [], // no options TODO
},
create: context => ({
JSXOpeningElement(node) {
if (isVisynIcon(node)) {
if (!hasTooltipProp(node)) {
context.report({ node, messageId: 'missingTooltip' });
} else if (isTooltipEmpty(node)) {
context.report({ node, messageId: 'emptyTooltip' });
}
}
},
})
}

export default EnforceVisynIconTooltip;

function isVisynIcon(node: TSESTree.JSXOpeningElement): boolean {
// Check if the component name matches a known icon component
if (node.name.type === 'JSXIdentifier' && ['VisynThemeIcon', 'VisynActionIcon'].includes(node.name.name)) {
return true;
}
return false;
}

function hasTooltipProp(node: TSESTree.JSXOpeningElement): boolean {
return node.attributes.some(attr => {
return attr.type === 'JSXAttribute' && attr.name.name === 'tooltip';
});
}

function isTooltipEmpty(node: TSESTree.JSXOpeningElement): boolean {
const tooltipAttr = node.attributes.find(attr => {
return attr.type === 'JSXAttribute' && attr.name.name === 'tooltip';
}) as TSESTree.JSXAttribute | undefined;

if (tooltipAttr && tooltipAttr.value) {
// tooltip as string
if (tooltipAttr.value.type === 'Literal' && typeof tooltipAttr.value.value === 'string') {
return tooltipAttr.value.value.trim() === '';
}

// tooltip as object with label property
if (tooltipAttr.value.type === 'JSXExpressionContainer') {
if (tooltipAttr.value.expression.type === 'ObjectExpression') {
const labelProp = tooltipAttr.value.expression.properties.find(prop => {
return prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'label';
}) as TSESTree.Property | undefined;
if (labelProp && labelProp.value.type === 'Literal' && typeof labelProp.value.value === 'string') {
return labelProp.value.value.trim() === '';
}
}
}
return false; // If tooltip value exists and is not empty
}
return true; // Consider empty if no value or unhandled type
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@
"@swc/jest": "~0.2.37",
"@types/jest": "~27.4.1",
"@types/node": "^20.17.14",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "~8.21.0",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@typescript-eslint/rule-tester": "7.18.0",
"@typescript-eslint/utils": "7.18.0",
"dotenv": "^16.4.7",
"dotenv-expand": "^12.0.1",
"dotenv-webpack": "^8.1.0",
Expand Down
83 changes: 83 additions & 0 deletions tests/enforce-icon-tooltip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// import { RuleTester } from '@typescript-eslint/rule-tester';
// import parser from '@typescript-eslint/parser';
// import rule from '../config/custom-eslint-rules/enforce-icon-tooltip';

const { RuleTester } = require('@typescript-eslint/utils');
const rule = require('./enforce-visyn-icon-tooltip').default;
const parser = require('@typescript-eslint/parser');

const ruleTester = new RuleTester({
languageOptions: {
parser,
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
});

ruleTester.run('enforce-icon-tooltip', rule, {
valid: [
// Case 1: VisynActionIcon with a valid, non-empty tooltip
{
code: '<VisynActionIcon icon={IconLasso} tooltip="Lasso Tool" />;',
},
// Case 2: VisynThemeIcon with a valid, non-empty tooltip
{
code: '<VisynThemeIcon icon={IconLasso} tooltip="Theme Icon" />;',
},
// Case 3: A valid tooltip as a variable
{
code: 'const tooltipText = "Hello"; <VisynActionIcon icon={IconLasso} tooltip={tooltipText} />;',
},
// Case 4: A valid tooltip with a dynamic expression
{
code: '<VisynActionIcon icon={IconLasso} tooltip={`Count: ${items.length}`} />;',
},
// Case 5: A valid tooltip as an object with a non-empty label property
{
code: '<VisynActionIcon icon={IconLasso} tooltip={{ label: "Lasso Tool", position: "top" }} />;',
},
],

invalid: [
// Case 1: VisynActionIcon with no tooltip prop
{
code: '<VisynActionIcon icon={IconLasso} />;',
errors: [{ messageId: 'missingTooltip' }],
},
// Case 2: VisynActionIcon with an empty string tooltip
{
code: '<VisynActionIcon icon={IconLasso} tooltip="" />;',
errors: [{ messageId: 'emptyTooltip' }],
},
// Case 3: VisynActionIcon with a tooltip prop containing only whitespace
{
code: '<VisynActionIcon icon={IconLasso} tooltip=" " />;',
errors: [{ messageId: 'emptyTooltip' }],
},
// Case 4: VisynThemeIcon with no tooltip prop
{
code: '<VisynThemeIcon icon={IconLasso} />;',
errors: [{ messageId: 'missingTooltip' }],
},
// Case 5: VisynThemeIcon with an empty string tooltip
{
code: '<VisynThemeIcon icon={IconLasso} tooltip="" />;',
errors: [{ messageId: 'emptyTooltip' }],
},
// Case 6: VisynActionIcon with a tooltip as an object with a label property containing only whitespace
{
code: '<VisynActionIcon icon={IconLasso} tooltip={{ label: " ", position: "top" }} />;',
errors: [{ messageId: 'emptyTooltip' }],
},
// Case 7: VisynThemeIcon with a tooltip as an object missing the label property
{
code: '<VisynThemeIcon icon={IconLasso} tooltip={{ position: "top" }} />;',
errors: [{ messageId: 'emptyTooltip' }],
},
],
});
Loading
Loading