diff --git a/docs/rules/no-restricted-tags.md b/docs/rules/no-restricted-tags.md
new file mode 100644
index 00000000..73f23cb7
--- /dev/null
+++ b/docs/rules/no-restricted-tags.md
@@ -0,0 +1,118 @@
+# no-restricted-tags
+
+This rule disallows the use of specified tags.
+
+## How to use
+
+```js,.eslintrc.js
+module.exports = {
+ rules: {
+ "@html-eslint/no-restricted-tags": [
+ "error",
+ {
+ tagPatterns: ["^div$", "^span$"],
+ message: "Use semantic elements instead of div and span.",
+ },
+ ],
+ },
+};
+```
+
+## Rule Details
+
+This rule allows you to specify tags that you don't want to use in your application.
+
+### Options
+
+This rule takes an array of option objects, where the `tagPatterns` are specified.
+
+- `tagPatterns`: An array of strings representing regular expression patterns. It disallows tag names that match any of the patterns.
+- `message` (optional): A custom error message to be shown when the rule is triggered.
+
+```js
+module.exports = {
+ rules: {
+ "@html-eslint/no-restricted-tags": [
+ "error",
+ {
+ tagPatterns: ["^div$", "^span$"],
+ message: "Use semantic elements instead of div and span.",
+ },
+ {
+ tagPatterns: ["h[1-6]"],
+ message: "Heading tags are not allowed in this context.",
+ },
+ {
+ tagPatterns: [".*-.*"],
+ message: "Custom elements should follow naming conventions.",
+ },
+ ],
+ },
+};
+```
+
+Examples of **incorrect** code for this rule with the option below:
+
+```json
+{
+ "tagPatterns": ["^div$", "^span$"],
+ "message": "Use semantic elements instead of generic containers"
+}
+```
+
+```html,incorrect
+
Content
+Text
+```
+
+Examples of **correct** code for this rule with the option above:
+
+```html,correct
+Content
+Text
+
+```
+
+Examples of **incorrect** code for this rule with regex patterns:
+
+```json
+{
+ "tagPatterns": ["h[1-6]"],
+ "message": "Heading tags are restricted"
+}
+```
+
+```html,incorrect
+Title
+Subtitle
+Section
+```
+
+Examples of **incorrect** code for this rule with custom element patterns:
+
+```json
+{
+ "tagPatterns": [".*-.*"],
+ "message": "Custom elements should follow naming conventions"
+}
+```
+
+```html,incorrect
+
+
+```
+
+Examples of **incorrect** code for this rule with deprecated tags:
+
+```json
+{
+ "tagPatterns": ["font|center|marquee"],
+ "message": "Deprecated HTML tags are not allowed"
+}
+```
+
+```html,incorrect
+Old style text
+Centered content
+
+```
diff --git a/packages/eslint-plugin/lib/rules/index.js b/packages/eslint-plugin/lib/rules/index.js
index 7712a3c3..fe0422b5 100644
--- a/packages/eslint-plugin/lib/rules/index.js
+++ b/packages/eslint-plugin/lib/rules/index.js
@@ -52,6 +52,7 @@ const noEmptyHeadings = require("./no-empty-headings");
const noInvalidEntity = require("./no-invalid-entity");
const noDuplicateInHead = require("./no-duplicate-in-head");
const noIneffectiveAttrs = require("./no-ineffective-attrs");
+const noRestrictedTags = require("./no-restricted-tags");
// import new rule here ↑
// DO NOT REMOVE THIS COMMENT
@@ -110,6 +111,7 @@ const rules = {
"no-invalid-entity": noInvalidEntity,
"no-duplicate-in-head": noDuplicateInHead,
"no-ineffective-attrs": noIneffectiveAttrs,
+ "no-restricted-tags": noRestrictedTags,
// export new rule here ↑
// DO NOT REMOVE THIS COMMENT
};
diff --git a/packages/eslint-plugin/lib/rules/no-restricted-tags.js b/packages/eslint-plugin/lib/rules/no-restricted-tags.js
new file mode 100644
index 00000000..ea684dd4
--- /dev/null
+++ b/packages/eslint-plugin/lib/rules/no-restricted-tags.js
@@ -0,0 +1,138 @@
+/**
+ * @import {StyleTag, Tag, ScriptTag} from "@html-eslint/types";
+ * @import {RuleModule} from "../types";
+ * @typedef {{tagPatterns: string[], message?: string}[]} Options
+ *
+ */
+
+const { NODE_TYPES } = require("@html-eslint/parser");
+const { RULE_CATEGORY } = require("../constants");
+const { createVisitors } = require("./utils/visitors");
+const { getRuleUrl } = require("./utils/rule");
+
+const MESSAGE_IDS = {
+ RESTRICTED: "restricted",
+};
+
+/**
+ * @type {RuleModule}
+ */
+module.exports = {
+ meta: {
+ type: "code",
+
+ docs: {
+ description: "Disallow specified tags",
+ category: RULE_CATEGORY.BEST_PRACTICE,
+ recommended: false,
+ url: getRuleUrl("no-restricted-tags"),
+ },
+
+ fixable: null,
+ schema: {
+ type: "array",
+
+ items: {
+ type: "object",
+ required: ["tagPatterns"],
+ properties: {
+ tagPatterns: {
+ type: "array",
+ items: {
+ type: "string",
+ },
+ },
+ message: {
+ type: "string",
+ },
+ },
+ additionalProperties: false,
+ },
+ },
+ messages: {
+ [MESSAGE_IDS.RESTRICTED]: "'{{tag}}' tag is restricted from being used.",
+ },
+ },
+
+ create(context) {
+ /**
+ * @type {Options}
+ */
+ const options = context.options;
+ const checkers = options.map((option) => new PatternChecker(option));
+
+ /**
+ * @param {Tag | StyleTag | ScriptTag} node
+ */
+ function check(node) {
+ const tagName =
+ node.type === NODE_TYPES.Tag
+ ? node.name
+ : node.type === NODE_TYPES.ScriptTag
+ ? "script"
+ : "style";
+
+ const matched = checkers.find((checker) => checker.test(tagName));
+
+ if (!matched) {
+ return;
+ }
+
+ /**
+ * @type {{node: Tag | StyleTag | ScriptTag, message: string, messageId?: string}}
+ */
+ const result = {
+ node: node,
+ message: "",
+ };
+
+ const customMessage = matched.getMessage();
+
+ if (customMessage) {
+ result.message = customMessage;
+ } else {
+ result.messageId = MESSAGE_IDS.RESTRICTED;
+ }
+
+ context.report({
+ ...result,
+ data: { tag: tagName },
+ });
+ }
+
+ return createVisitors(context, {
+ Tag: check,
+ StyleTag: check,
+ ScriptTag: check,
+ });
+ },
+};
+
+class PatternChecker {
+ /**
+ * @param {Options[number]} option
+ */
+ constructor(option) {
+ this.option = option;
+ this.tagRegExps = option.tagPatterns.map(
+ (pattern) => new RegExp(pattern, "u")
+ );
+ this.message = option.message;
+ }
+
+ /**
+ * @param {string} tagName
+ * @returns {boolean}
+ */
+ test(tagName) {
+ const result = this.tagRegExps.some((exp) => exp.test(tagName));
+ return result;
+ }
+
+ /**
+ * @returns {string}
+ */
+ getMessage() {
+ return this.message || "";
+ }
+}
diff --git a/packages/eslint-plugin/tests/rules/no-restricted-tags.test.js b/packages/eslint-plugin/tests/rules/no-restricted-tags.test.js
new file mode 100644
index 00000000..f57ee53e
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/no-restricted-tags.test.js
@@ -0,0 +1,271 @@
+const createRuleTester = require("../rule-tester");
+const rule = require("../../lib/rules/no-restricted-tags");
+
+const ruleTester = createRuleTester();
+const templateRuleTester = createRuleTester("espree");
+
+ruleTester.run("no-restricted-tags", rule, {
+ valid: [
+ {
+ code: `
`,
+ options: [
+ {
+ tagPatterns: ["span"],
+ },
+ ],
+ },
+ {
+ code: `content
`,
+ options: [
+ {
+ tagPatterns: ["div"],
+ },
+ ],
+ },
+ {
+ code: ``,
+ options: [
+ {
+ tagPatterns: ["footer"],
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: `
`,
+ options: [
+ {
+ tagPatterns: ["div"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "div",
+ },
+ },
+ ],
+ },
+ {
+ code: `text`,
+ options: [
+ {
+ tagPatterns: ["span"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "span",
+ },
+ },
+ ],
+ },
+ {
+ code: ``,
+ options: [
+ {
+ tagPatterns: ["script"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "script",
+ },
+ },
+ ],
+ },
+ {
+ code: ``,
+ options: [
+ {
+ tagPatterns: ["style"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "style",
+ },
+ },
+ ],
+ },
+ // regex patterns
+ {
+ code: `
`,
+ options: [
+ {
+ tagPatterns: ["^div$"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "div",
+ },
+ },
+ ],
+ },
+ {
+ code: ` `,
+ options: [
+ {
+ tagPatterns: [".*-.*"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "custom-element",
+ },
+ },
+ {
+ messageId: "restricted",
+ data: {
+ tag: "another-custom",
+ },
+ },
+ ],
+ },
+ {
+ code: `Title
Subtitle
Section
`,
+ options: [
+ {
+ tagPatterns: ["h[1-6]"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "h1",
+ },
+ },
+ {
+ messageId: "restricted",
+ data: {
+ tag: "h2",
+ },
+ },
+ {
+ messageId: "restricted",
+ data: {
+ tag: "h3",
+ },
+ },
+ ],
+ },
+ // custom message
+ {
+ code: `
`,
+ options: [
+ {
+ tagPatterns: ["div"],
+ message: "Do not use div tags, use semantic elements instead",
+ },
+ ],
+ errors: [
+ {
+ message: "Do not use div tags, use semantic elements instead",
+ },
+ ],
+ },
+ {
+ code: `old style`,
+ options: [
+ {
+ tagPatterns: ["font|center|marquee"],
+ message: "Deprecated HTML tags are not allowed",
+ },
+ ],
+ errors: [
+ {
+ message: "Deprecated HTML tags are not allowed",
+ },
+ ],
+ },
+ // multiple patterns
+ {
+ code: ` `,
+ options: [
+ {
+ tagPatterns: ["div", "span"],
+ },
+ {
+ tagPatterns: [".*-.*"],
+ message: "Custom elements should follow naming conventions",
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "div",
+ },
+ },
+ {
+ messageId: "restricted",
+ data: {
+ tag: "span",
+ },
+ },
+ {
+ message: "Custom elements should follow naming conventions",
+ },
+ ],
+ },
+ ],
+});
+
+templateRuleTester.run("[template] no-restricted-tags", rule, {
+ valid: [
+ {
+ code: `html\`content
\``,
+ options: [
+ {
+ tagPatterns: ["div"],
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: `html\`content
\``,
+ options: [
+ {
+ tagPatterns: ["div"],
+ message: "Do not use div in templates",
+ },
+ ],
+ errors: [
+ {
+ message: "Do not use div in templates",
+ },
+ ],
+ },
+ {
+ code: `html\`\``,
+ options: [
+ {
+ tagPatterns: [".*-.*"],
+ },
+ ],
+ errors: [
+ {
+ messageId: "restricted",
+ data: {
+ tag: "custom-element",
+ },
+ },
+ ],
+ },
+ ],
+});