Repository: protofire/solhint
Pull request #689 (2025-07-02 18:17:09)

Feature use natspec
# use-natspec

## Description
Enforces the presence and correctness of NatSpec tags.

## Options
This rule accepts an array of options:

| Index | Description                                                              | Default Value        |
| ----- | ------------------------------------------------------------------------ | -------------------- |
| 0     | Rule severity. Must be one of "error", "warn", "off".                    | warn                 |
| 1     | A JSON object with natspec properties. See EXAMPLE CONFIG section below. | Check EXAMPLE CONFIG |


### Example Config
```json
{
  "rules": {
    "use-natspec": [
      "warn",
      {
        "title": {
          "enabled": true,
          "ignore": {}
        },
        "author": {
          "enabled": true,
          "ignore": {}
        },
        "notice": {
          "enabled": true,
          "ignore": {}
        },
        "param": {
          "enabled": true,
          "ignore": {}
        },
        "return": {
          "enabled": true,
          "ignore": {}
        }
      }
    ]
  }
}
```

### Notes
- If a function or return value has unnamed parameters (e.g. `function foo(uint256)`), the rule only checks the number of `@param` or `@return` tags, not their names.
- If a function or variable has `@inheritdoc`, the rule skips the validation.
- The rule supports both `///` and `/** */` style NatSpec comments.
- If a custom config is provided, it is merged with the default config. Only the overridden fields need to be specified.

## Examples
### 👍 Examples of **correct** code for this rule

#### Contract with valid NatSpec

```solidity

                /// @title Token contract
                /// @author Me
                /// @notice This contract handles tokens
                contract Token {
                  /// @notice Transfers tokens
                  /// @param to The recipient address
                  /// @param amount The amount to transfer
                  /// @return success Whether the transfer succeeded
                  function transfer(address to, uint256 amount) public returns (bool success) {
                    return true;
                  }
                }
              
```

#### You can disable specific tags globally or by type/name using the `ignore` option in the config. For example:

```solidity
{
                "use-natspec": [
                  "warn",
                  {
                    "title": {
                      "enabled": true,
                      "ignore": {
                        "contract": ["MyContract"],
                        "*": ["LegacyContract"]
                      }
                    },
                    "param": { "enabled": false }
                  }
                ]
              }
```

#### The default configuration enables all checks with no ignore rules:

```solidity
{
                "title": { "enabled": true, "ignore": {} },
                "author": { "enabled": true, "ignore": {} },
                "notice": { "enabled": true, "ignore": {} },
                "param": { "enabled": true, "ignore": {} },
                "return": { "enabled": true, "ignore": {} }
              }
```

### 👎 Examples of **incorrect** code for this rule

#### Missing @notice and incorrect @param and @return tags

```solidity

                /// @title Token contract
                contract Token {
                  /// @param wrongParam Not matching actual parameter
                  function transfer(address to) public returns (bool) {
                    return true;
                  }
                }
                
```



Patch:
diff --git a/conf/rulesets/solhint-all.js b/conf/rulesets/solhint-all.js
index 3e6a635..3a659a5 100644
--- a/conf/rulesets/solhint-all.js
+++ b/conf/rulesets/solhint-all.js
@@ -24,6 +24,31 @@ module.exports = Object.freeze({
         maxLength: 32,
       },
     ],
+    'use-natspec': [
+      'warn',
+      {
+        title: {
+          enabled: true,
+          ignore: {},
+        },
+        author: {
+          enabled: true,
+          ignore: {},
+        },
+        notice: {
+          enabled: true,
+          ignore: {},
+        },
+        param: {
+          enabled: true,
+          ignore: {},
+        },
+        return: {
+          enabled: true,
+          ignore: {},
+        },
+      },
+    ],
     'constructor-syntax': 'warn',
     'gas-calldata-parameters': 'warn',
     'gas-custom-errors': 'warn',
diff --git a/docs/rules.md b/docs/rules.md
index 23d55ee..0669d53 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -21,6 +21,7 @@ title:       "Rule Index of Solhint"
 | [one-contract-per-file](./rules/best-practices/one-contract-per-file.md) | Enforces the use of ONE Contract per file see [here](https://docs.soliditylang.org/en/v0.8.21/style-guide.html#contract-and-library-names) | $~~~~~~~~$✔️ |            |
 | [payable-fallback](./rules/best-practices/payable-fallback.md)           | When fallback is not payable and there is no receive function you will not be able to receive currency.                                    | $~~~~~~~~$✔️ |            |
 | [reason-string](./rules/best-practices/reason-string.md)                 | Require or revert statement must have a reason string and check that each reason string is at most N characters long.                      | $~~~~~~~~$✔️ |            |
+| [use-natspec](./rules/best-practices/use-natspec.md)                     | Enforces the presence and correctness of NatSpec tags.                                                                                     |              |            |
 | [constructor-syntax](./rules/best-practices/constructor-syntax.md)       | Constructors should use the new constructor keyword.                                                                                       |              |            |
         
 
diff --git a/docs/rules/best-practices/code-complexity.md b/docs/rules/best-practices/code-complexity.md
index 5076ada..f62246b 100644
--- a/docs/rules/best-practices/code-complexity.md
+++ b/docs/rules/best-practices/code-complexity.md
@@ -24,7 +24,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "code-complexity": ["warn",7]
+    "code-complexity": [
+      "warn",
+      7
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/explicit-types.md b/docs/rules/best-practices/explicit-types.md
index 1e8cd62..7c13a8f 100644
--- a/docs/rules/best-practices/explicit-types.md
+++ b/docs/rules/best-practices/explicit-types.md
@@ -27,7 +27,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "explicit-types": ["warn","explicit"]
+    "explicit-types": [
+      "warn",
+      "explicit"
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/function-max-lines.md b/docs/rules/best-practices/function-max-lines.md
index f5e86e3..9d5d8a6 100644
--- a/docs/rules/best-practices/function-max-lines.md
+++ b/docs/rules/best-practices/function-max-lines.md
@@ -24,7 +24,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "function-max-lines": ["warn",50]
+    "function-max-lines": [
+      "warn",
+      50
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/max-line-length.md b/docs/rules/best-practices/max-line-length.md
index 5c133ea..d950331 100644
--- a/docs/rules/best-practices/max-line-length.md
+++ b/docs/rules/best-practices/max-line-length.md
@@ -26,7 +26,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "max-line-length": ["error",120]
+    "max-line-length": [
+      "error",
+      120
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/max-states-count.md b/docs/rules/best-practices/max-states-count.md
index cd39de6..a50aa4a 100644
--- a/docs/rules/best-practices/max-states-count.md
+++ b/docs/rules/best-practices/max-states-count.md
@@ -27,7 +27,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "max-states-count": ["warn",15]
+    "max-states-count": [
+      "warn",
+      15
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/reason-string.md b/docs/rules/best-practices/reason-string.md
index 4e22fae..e2320f2 100644
--- a/docs/rules/best-practices/reason-string.md
+++ b/docs/rules/best-practices/reason-string.md
@@ -27,7 +27,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "reason-string": ["warn",{"maxLength":32}]
+    "reason-string": [
+      "warn",
+      {
+        "maxLength": 32
+      }
+    ]
   }
 }
 ```
diff --git a/docs/rules/best-practices/use-natspec.md b/docs/rules/best-practices/use-natspec.md
new file mode 100644
index 0000000..538712f
--- /dev/null
+++ b/docs/rules/best-practices/use-natspec.md
@@ -0,0 +1,138 @@
+---
+warning:     "This is a dynamically generated file. Do not edit manually."
+layout:      "default"
+title:       "use-natspec | Solhint"
+---
+
+# use-natspec
+![Category Badge](https://img.shields.io/badge/-Best%20Practices%20Rules-informational)
+![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
+
+## Description
+Enforces the presence and correctness of NatSpec tags.
+
+## Options
+This rule accepts an array of options:
+
+| Index | Description                                                              | Default Value        |
+| ----- | ------------------------------------------------------------------------ | -------------------- |
+| 0     | Rule severity. Must be one of "error", "warn", "off".                    | warn                 |
+| 1     | A JSON object with natspec properties. See EXAMPLE CONFIG section below. | Check EXAMPLE CONFIG |
+
+
+### Example Config
+```json
+{
+  "rules": {
+    "use-natspec": [
+      "warn",
+      {
+        "title": {
+          "enabled": true,
+          "ignore": {}
+        },
+        "author": {
+          "enabled": true,
+          "ignore": {}
+        },
+        "notice": {
+          "enabled": true,
+          "ignore": {}
+        },
+        "param": {
+          "enabled": true,
+          "ignore": {}
+        },
+        "return": {
+          "enabled": true,
+          "ignore": {}
+        }
+      }
+    ]
+  }
+}
+```
+
+### Notes
+- If a function or return value has unnamed parameters (e.g. `function foo(uint256)`), the rule only checks the number of `@param` or `@return` tags, not their names.
+- If a function or variable has `@inheritdoc`, the rule skips the validation.
+- The rule supports both `///` and `/** */` style NatSpec comments.
+- If a custom config is provided, it is merged with the default config. Only the overridden fields need to be specified.
+
+## Examples
+### 👍 Examples of **correct** code for this rule
+
+#### Contract with valid NatSpec
+
+```solidity
+
+                /// @title Token contract
+                /// @author Me
+                /// @notice This contract handles tokens
+                contract Token {
+                  /// @notice Transfers tokens
+                  /// @param to The recipient address
+                  /// @param amount The amount to transfer
+                  /// @return success Whether the transfer succeeded
+                  function transfer(address to, uint256 amount) public returns (bool success) {
+                    return true;
+                  }
+                }
+              
+```
+
+#### You can disable specific tags globally or by type/name using the `ignore` option in the config. For example:
+
+```solidity
+{
+                "use-natspec": [
+                  "warn",
+                  {
+                    "title": {
+                      "enabled": true,
+                      "ignore": {
+                        "contract": ["MyContract"],
+                        "*": ["LegacyContract"]
+                      }
+                    },
+                    "param": { "enabled": false }
+                  }
+                ]
+              }
+```
+
+#### The default configuration enables all checks with no ignore rules:
+
+```solidity
+{
+                "title": { "enabled": true, "ignore": {} },
+                "author": { "enabled": true, "ignore": {} },
+                "notice": { "enabled": true, "ignore": {} },
+                "param": { "enabled": true, "ignore": {} },
+                "return": { "enabled": true, "ignore": {} }
+              }
+```
+
+### 👎 Examples of **incorrect** code for this rule
+
+#### Missing @notice and incorrect @param and @return tags
+
+```solidity
+
+                /// @title Token contract
+                contract Token {
+                  /// @param wrongParam Not matching actual parameter
+                  function transfer(address to) public returns (bool) {
+                    return true;
+                  }
+                }
+                
+```
+
+## Version
+This rule was introduced in the latest version.
+
+## Resources
+- [Rule source](https://github.com/protofire/solhint/blob/master/lib/rules/best-practices/use-natspec.js)
+- [Document source](https://github.com/protofire/solhint/blob/master/docs/rules/best-practices/use-natspec.md)
+- [Test cases](https://github.com/protofire/solhint/blob/master/test/rules/best-practices/use-natspec.js)
diff --git a/docs/rules/miscellaneous/import-path-check.md b/docs/rules/miscellaneous/import-path-check.md
index 52a8253..5ba9842 100644
--- a/docs/rules/miscellaneous/import-path-check.md
+++ b/docs/rules/miscellaneous/import-path-check.md
@@ -27,7 +27,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "import-path-check": ["warn",["[~dependenciesPath]"]]
+    "import-path-check": [
+      "warn",
+      [
+        "[~dependenciesPath]"
+      ]
+    ]
   }
 }
 ```
diff --git a/docs/rules/miscellaneous/quotes.md b/docs/rules/miscellaneous/quotes.md
index 54e9e07..cdc0925 100644
--- a/docs/rules/miscellaneous/quotes.md
+++ b/docs/rules/miscellaneous/quotes.md
@@ -27,7 +27,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "quotes": ["error","double"]
+    "quotes": [
+      "error",
+      "double"
+    ]
   }
 }
 ```
diff --git a/docs/rules/naming/func-named-parameters.md b/docs/rules/naming/func-named-parameters.md
index b5b0a10..b41d087 100644
--- a/docs/rules/naming/func-named-parameters.md
+++ b/docs/rules/naming/func-named-parameters.md
@@ -24,7 +24,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "func-named-parameters": ["warn",4]
+    "func-named-parameters": [
+      "warn",
+      4
+    ]
   }
 }
 ```
diff --git a/docs/rules/naming/immutable-vars-naming.md b/docs/rules/naming/immutable-vars-naming.md
index d7d301b..57c1453 100644
--- a/docs/rules/naming/immutable-vars-naming.md
+++ b/docs/rules/naming/immutable-vars-naming.md
@@ -27,7 +27,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "immutable-vars-naming": ["warn",{"immutablesAsConstants":true}]
+    "immutable-vars-naming": [
+      "warn",
+      {
+        "immutablesAsConstants": true
+      }
+    ]
   }
 }
 ```
diff --git a/docs/rules/naming/private-vars-leading-underscore.md b/docs/rules/naming/private-vars-leading-underscore.md
index 909c43c..7366bf3 100644
--- a/docs/rules/naming/private-vars-leading-underscore.md
+++ b/docs/rules/naming/private-vars-leading-underscore.md
@@ -24,7 +24,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "private-vars-leading-underscore": ["warn",{"strict":false}]
+    "private-vars-leading-underscore": [
+      "warn",
+      {
+        "strict": false
+      }
+    ]
   }
 }
 ```
diff --git a/docs/rules/naming/var-name-mixedcase.md b/docs/rules/naming/var-name-mixedcase.md
index 38eec98..cc1e626 100644
--- a/docs/rules/naming/var-name-mixedcase.md
+++ b/docs/rules/naming/var-name-mixedcase.md
@@ -27,7 +27,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "var-name-mixedcase": ["warn",{"prefixForImmutables":"IMM_"}]
+    "var-name-mixedcase": [
+      "warn",
+      {
+        "prefixForImmutables": "IMM_"
+      }
+    ]
   }
 }
 ```
diff --git a/docs/rules/security/compiler-version.md b/docs/rules/security/compiler-version.md
index a80f364..fab7d63 100644
--- a/docs/rules/security/compiler-version.md
+++ b/docs/rules/security/compiler-version.md
@@ -27,7 +27,10 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "compiler-version": ["error","^0.8.24"]
+    "compiler-version": [
+      "error",
+      "^0.8.24"
+    ]
   }
 }
 ```
diff --git a/docs/rules/security/func-visibility.md b/docs/rules/security/func-visibility.md
index c85eda8..c781fe8 100644
--- a/docs/rules/security/func-visibility.md
+++ b/docs/rules/security/func-visibility.md
@@ -27,7 +27,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "func-visibility": ["warn",{"ignoreConstructors":false}]
+    "func-visibility": [
+      "warn",
+      {
+        "ignoreConstructors": false
+      }
+    ]
   }
 }
 ```
diff --git a/lib/config.js b/lib/config.js
index 6983683..6485818 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -53,4 +53,10 @@ module.exports = {
     const configVal = _.get(this, path)
     return _.isArray(configVal) ? configVal : defaultValue
   },
+
+  getObject(ruleName, defaultValue) {
+    const path = `rules["${ruleName}"][1]`
+    const configVal = _.get(this, path)
+    return _.isObject(configVal) ? configVal : defaultValue
+  },
 }
diff --git a/lib/rules/best-practices/index.js b/lib/rules/best-practices/index.js
index c7d2f09..1644d6f 100644
--- a/lib/rules/best-practices/index.js
+++ b/lib/rules/best-practices/index.js
@@ -12,6 +12,7 @@ const NoUnusedImportsChecker = require('./no-unused-import')
 const ExplicitTypesChecker = require('./explicit-types')
 const OneContractPerFileChecker = require('./one-contract-per-file')
 const InterfaceStartsWithIChecker = require('./interface-starts-with-i')
+const UseNatspecChecker = require('./use-natspec')
 
 module.exports = function checkers(reporter, config, inputSrc, tokens) {
   return [
@@ -29,5 +30,6 @@ module.exports = function checkers(reporter, config, inputSrc, tokens) {
     new ExplicitTypesChecker(reporter, config),
     new OneContractPerFileChecker(reporter),
     new InterfaceStartsWithIChecker(reporter),
+    new UseNatspecChecker(reporter, config, tokens),
   ]
 }
diff --git a/lib/rules/best-practices/use-natspec.js b/lib/rules/best-practices/use-natspec.js
new file mode 100644
index 0000000..7b28967
--- /dev/null
+++ b/lib/rules/best-practices/use-natspec.js
@@ -0,0 +1,435 @@
+const _ = require('lodash')
+const BaseChecker = require('../base-checker')
+const { severityDescription } = require('../../doc/utils')
+
+const ruleId = 'use-natspec'
+
+const DEFAULT_SEVERITY = 'warn'
+const VALID_TAGS = new Set(['title', 'author', 'notice', 'param', 'return', 'inheritdoc'])
+const DEFAULT_CONFIG = {
+  title: { enabled: true, ignore: {} },
+  author: { enabled: true, ignore: {} },
+  notice: { enabled: true, ignore: {} },
+  param: { enabled: true, ignore: {} },
+  return: { enabled: true, ignore: {} },
+}
+
+const NATSPEC_TARGETS = {
+  contract: ['title', 'author', 'notice'],
+  library: ['title', 'author', 'notice'],
+  interface: ['title', 'author', 'notice'],
+  function: ['notice', 'param', 'return'],
+  event: ['notice', 'param'],
+  variable: ['notice'],
+}
+
+const META_SCHEMA = {
+  type: 'object',
+  properties: {
+    title: {
+      type: 'object',
+      properties: {
+        enabled: { type: 'boolean' },
+        ignore: {
+          type: 'object',
+          patternProperties: {
+            '.*': {
+              type: 'array',
+              items: { type: 'string' },
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+      required: ['enabled'],
+      additionalProperties: false,
+    },
+    author: {
+      type: 'object',
+      properties: {
+        enabled: { type: 'boolean' },
+        ignore: {
+          type: 'object',
+          patternProperties: {
+            '.*': {
+              type: 'array',
+              items: { type: 'string' },
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+      required: ['enabled'],
+      additionalProperties: false,
+    },
+    notice: {
+      type: 'object',
+      properties: {
+        enabled: { type: 'boolean' },
+        ignore: {
+          type: 'object',
+          patternProperties: {
+            '.*': {
+              type: 'array',
+              items: { type: 'string' },
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+      required: ['enabled'],
+      additionalProperties: false,
+    },
+    param: {
+      type: 'object',
+      properties: {
+        enabled: { type: 'boolean' },
+        ignore: {
+          type: 'object',
+          patternProperties: {
+            '.*': {
+              type: 'array',
+              items: { type: 'string' },
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+      required: ['enabled'],
+      additionalProperties: false,
+    },
+    return: {
+      type: 'object',
+      properties: {
+        enabled: { type: 'boolean' },
+        ignore: {
+          type: 'object',
+          patternProperties: {
+            '.*': {
+              type: 'array',
+              items: { type: 'string' },
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+      required: ['enabled'],
+      additionalProperties: false,
+    },
+  },
+  additionalProperties: false,
+}
+
+const meta = {
+  type: 'best-practices',
+
+  docs: {
+    description: 'Enforces the presence and correctness of NatSpec tags.',
+    category: 'Best Practices Rules',
+    options: [
+      {
+        description: severityDescription,
+        default: DEFAULT_SEVERITY,
+      },
+      {
+        description: 'A JSON object with natspec properties. See EXAMPLE CONFIG section below.',
+        default: 'Check EXAMPLE CONFIG',
+      },
+    ],
+
+    examples: {
+      good: [
+        {
+          description: 'Contract with valid NatSpec',
+          code: `
+                /// @title Token contract
+                /// @author Me
+                /// @notice This contract handles tokens
+                contract Token {
+                  /// @notice Transfers tokens
+                  /// @param to The recipient address
+                  /// @param amount The amount to transfer
+                  /// @return success Whether the transfer succeeded
+                  function transfer(address to, uint256 amount) public returns (bool success) {
+                    return true;
+                  }
+                }
+              `,
+        },
+        {
+          description:
+            'You can disable specific tags globally or by type/name using the `ignore` option in the config. For example:',
+          code: `{
+                "use-natspec": [
+                  "warn",
+                  {
+                    "title": {
+                      "enabled": true,
+                      "ignore": {
+                        "contract": ["MyContract"],
+                        "*": ["LegacyContract"]
+                      }
+                    },
+                    "param": { "enabled": false }
+                  }
+                ]
+              }`,
+        },
+        {
+          description: 'The default configuration enables all checks with no ignore rules:',
+          code: `{
+                "title": { "enabled": true, "ignore": {} },
+                "author": { "enabled": true, "ignore": {} },
+                "notice": { "enabled": true, "ignore": {} },
+                "param": { "enabled": true, "ignore": {} },
+                "return": { "enabled": true, "ignore": {} }
+              }`,
+        },
+      ],
+      bad: [
+        {
+          description: 'Missing @notice and incorrect @param and @return tags',
+          code: `
+                /// @title Token contract
+                contract Token {
+                  /// @param wrongParam Not matching actual parameter
+                  function transfer(address to) public returns (bool) {
+                    return true;
+                  }
+                }
+                `,
+        },
+      ],
+    },
+    notes: [
+      {
+        note: 'If a function or return value has unnamed parameters (e.g. `function foo(uint256)`), the rule only checks the number of `@param` or `@return` tags, not their names.',
+      },
+      {
+        note: 'If a function or variable has `@inheritdoc`, the rule skips the validation.',
+      },
+      {
+        note: 'The rule supports both `///` and `/** */` style NatSpec comments.',
+      },
+      {
+        note: 'If a custom config is provided, it is merged with the default config. Only the overridden fields need to be specified.',
+      },
+    ],
+  },
+
+  recommended: false,
+  defaultSetup: ['warn', DEFAULT_CONFIG],
+
+  schema: META_SCHEMA,
+}
+
+class UseNatspecChecker extends BaseChecker {
+  constructor(reporter, config, tokens) {
+    super(reporter, ruleId, meta)
+    if (!this.enabled) return // if the rule is disabled, do nothing
+
+    this.tokens = tokens
+    const userConfig = config?.getObject(ruleId) || {}
+    this.tagConfig = _.merge({}, DEFAULT_CONFIG, userConfig)
+  }
+
+  ContractDefinition(node) {
+    this.checkNode(node, 'contract')
+  }
+
+  LibraryDefinition(node) {
+    this.checkNode(node, 'library')
+  }
+
+  InterfaceDefinition(node) {
+    this.checkNode(node, 'interface')
+  }
+
+  FunctionDefinition(node) {
+    const visibility = node.visibility || 'default'
+    const isPublicLike = visibility === 'public' || visibility === 'external'
+    const tags = [...NATSPEC_TARGETS.function]
+
+    if (!hasParameters(node)) tags.splice(tags.indexOf('param'), 1)
+    if (!hasReturn(node)) tags.splice(tags.indexOf('return'), 1)
+
+    this.checkNode(node, 'function', tags, isPublicLike)
+  }
+
+  EventDefinition(node) {
+    const tags = [...NATSPEC_TARGETS.event]
+    if (!hasParameters(node)) tags.splice(tags.indexOf('param'), 1)
+    this.checkNode(node, 'event', tags)
+  }
+
+  VariableDeclaration(node) {
+    if (!node.isStateVar || node.visibility !== 'public') return
+    this.checkNode(node, 'variable', NATSPEC_TARGETS.variable, true)
+  }
+
+  checkNode(node, type, overrideTags, requireNoticeIfPublic = false) {
+    const name = node.name || (node.id && node.id.name) || '<anonymous>'
+    const tagsRequired = overrideTags || NATSPEC_TARGETS[type] || []
+    const startOffset = node.range[0]
+    const comments = getLeadingNatSpecComments(startOffset, this.tokens)
+    const tags = extractNatSpecTags(comments)
+
+    // console.log('\n>>> CHECKING', type, name)
+    // console.log('COMMENTS:', comments)
+    // console.log('TAGS:', tags)
+
+    const hasInheritdoc = tags.includes('inheritdoc')
+    if (requireNoticeIfPublic && hasInheritdoc) return
+
+    for (const tag of tagsRequired) {
+      const rule = this.tagConfig[tag]
+      if (!rule?.enabled) continue
+      if (!shouldValidateTag(tag, type, name, this.tagConfig)) continue
+
+      if (!tags.includes(tag)) {
+        this.reporter.warn(node, ruleId, `Missing @${tag} tag in ${type} '${name}'`)
+      }
+
+      if (tag === 'param') {
+        const docParams = extractTagNames(comments, 'param')
+        const solidityParams = node.parameters || []
+
+        const namedParams = solidityParams
+          .map((p) => p.name)
+          .filter((name) => typeof name === 'string' && name.length > 0)
+
+        const allParamsHaveNames = namedParams.length === solidityParams.length
+
+        if (allParamsHaveNames) {
+          if (namedParams.length !== docParams.length || !arraysEqual(namedParams, docParams)) {
+            this.reporter.warn(
+              node,
+              ruleId,
+              `Mismatch in @param names for ${type} '${name}'. Expected: [${namedParams.join(
+                ', '
+              )}], Found: [${docParams.join(', ')}]`
+            )
+          }
+        } else if (solidityParams.length !== docParams.length) {
+          this.reporter.warn(
+            node,
+            ruleId,
+            `Mismatch in @param count for ${type} '${name}'. Expected: ${solidityParams.length}, Found: ${docParams.length}`
+          )
+        }
+      }
+
+      if (tag === 'return') {
+        const docReturns = extractTagNames(comments, 'return')
+        const solidityReturns = node.returnParameters || []
+
+        const namedReturns = solidityReturns
+          .map((p) => p.name)
+          .filter((name) => typeof name === 'string' && name.length > 0)
+
+        const allReturnsHaveNames = namedReturns.length === solidityReturns.length
+
+        if (allReturnsHaveNames) {
+          if (namedReturns.length !== docReturns.length || !arraysEqual(namedReturns, docReturns)) {
+            this.reporter.warn(
+              node,
+              ruleId,
+              `Mismatch in @return names for ${type} '${name}'. Expected: [${namedReturns.join(
+                ', '
+              )}], Found: [${docReturns.join(', ')}]`
+            )
+          }
+        } else if (solidityReturns.length !== docReturns.length) {
+          this.reporter.warn(
+            node,
+            ruleId,
+            `Mismatch in @return count for ${type} '${name}'. Expected: ${solidityReturns.length}, Found: ${docReturns.length}`
+          )
+        }
+      }
+    }
+  }
+}
+
+function getLeadingNatSpecComments(startOffset, tokens) {
+  const comments = []
+
+  for (let i = tokens.length - 1; i >= 0; i--) {
+    const token = tokens[i]
+
+    if (token.range[1] > startOffset) continue
+
+    const value = token.value?.trim()
+    if (
+      typeof value === 'string' &&
+      (value.startsWith('///') || value.startsWith('/**')) &&
+      value.includes('@') &&
+      hasAnyNatSpecTag(value)
+    ) {
+      comments.unshift(value)
+    }
+
+    // Si encontramos código real (no comentario ni puntuación), cortamos
+    if (!value || (!value.startsWith('///') && !value.startsWith('/**'))) {
+      if (token.type !== 'Punctuator') break
+    }
+  }
+
+  return comments
+}
+
+function extractTagNames(comments, tag) {
+  const names = []
+  const regex = new RegExp(`@${tag}\\s+(\\w+)`, 'g')
+
+  for (const comment of comments) {
+    let match = regex.exec(comment)
+    while (match !== null) {
+      names.push(match[1])
+      match = regex.exec(comment)
+    }
+  }
+
+  return names
+}
+
+function arraysEqual(arr1, arr2) {
+  if (arr1.length !== arr2.length) return false
+  return arr1.every((v, i) => v === arr2[i])
+}
+
+function hasAnyNatSpecTag(commentValue) {
+  for (const [, tag] of commentValue.matchAll(/@([a-zA-Z0-9_]+)/g)) {
+    if (VALID_TAGS.has(tag)) return true
+  }
+  return false
+}
+
+function extractNatSpecTags(comments) {
+  const tags = new Set()
+  for (const comment of comments) {
+    const matches = comment.matchAll(/@([a-zA-Z0-9_]+)/g)
+    for (const [, tag] of matches) {
+      tags.add(tag)
+    }
+  }
+  return [...tags]
+}
+
+function shouldValidateTag(tag, type, name, config) {
+  const tagConf = config[tag]
+  if (!tagConf || !tagConf.enabled) return false
+  const ignoreByType = tagConf.ignore?.[type] || []
+  const ignoreGlobal = tagConf.ignore?.['*'] || []
+  return !ignoreByType.includes(name) && !ignoreGlobal.includes(name)
+}
+
+function hasParameters(node) {
+  return node.parameters && node.parameters.length > 0
+}
+
+function hasReturn(node) {
+  return node.returnParameters && node.returnParameters.length > 0
+}
+
+module.exports = UseNatspecChecker
diff --git a/scripts/generate-rule-docs.js b/scripts/generate-rule-docs.js
index 5501c21..1012287 100755
--- a/scripts/generate-rule-docs.js
+++ b/scripts/generate-rule-docs.js
@@ -158,12 +158,14 @@ ${table(optionsTable)}
 }
 
 function loadExampleConfig(rule) {
-  return `\`\`\`json
-{
-  "rules": {
-    "${rule.ruleId}": ${JSON.stringify(rule.meta.defaultSetup)}
+  const fullConfig = {
+    rules: {
+      [rule.ruleId]: rule.meta.defaultSetup,
+    },
   }
-}
+
+  return `\`\`\`json
+${JSON.stringify(fullConfig, null, 2)}
 \`\`\`
 `
 }


Test patch:
diff --git a/docs/rules/naming/foundry-test-functions.md b/docs/rules/naming/foundry-test-functions.md
index 37a9b01..1fa0272 100644
--- a/docs/rules/naming/foundry-test-functions.md
+++ b/docs/rules/naming/foundry-test-functions.md
@@ -24,7 +24,12 @@ This rule accepts an array of options:
 ```json
 {
   "rules": {
-    "foundry-test-functions": ["warn",["setUp"]]
+    "foundry-test-functions": [
+      "warn",
+      [
+        "setUp"
+      ]
+    ]
   }
 }
 ```
diff --git a/e2e/05-max-warnings/.solhint.json b/e2e/05-max-warnings/.solhint.json
index 3b8ee84..28e109d 100644
--- a/e2e/05-max-warnings/.solhint.json
+++ b/e2e/05-max-warnings/.solhint.json
@@ -1,3 +1,6 @@
 {
-  "extends": "solhint:all"
+  "extends": "solhint:all",
+  "rules": {
+    "use-natspec": "off"
+  }
 }
diff --git a/e2e/06-formatters/.solhint.json b/e2e/06-formatters/.solhint.json
index 3b8ee84..28e109d 100644
--- a/e2e/06-formatters/.solhint.json
+++ b/e2e/06-formatters/.solhint.json
@@ -1,3 +1,6 @@
 {
-  "extends": "solhint:all"
+  "extends": "solhint:all",
+  "rules": {
+    "use-natspec": "off"
+  }
 }
diff --git a/test/common/config-validator.js b/test/common/config-validator.js
index 97abcc2..710e581 100644
--- a/test/common/config-validator.js
+++ b/test/common/config-validator.js
@@ -155,6 +155,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'compiler-version'"),
       `Expected a warning for compiler-version but got:\n${logged}`
@@ -178,6 +179,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'max-line-length'"),
       `Expected a warning for max-line-length but got:\n${logged}`
@@ -201,6 +203,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'interface-starts-with-i'"),
       `Expected a warning for interface-starts-with-i but got:\n${logged}`
@@ -224,6 +227,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'interface-starts-with-i'"),
       `Expected a warning for interface-starts-with-i but got:\n${logged}`
@@ -247,6 +251,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("[solhint] Warning: Rule 'interface-sta' doesn't exist"),
       `Expected a warning for interface-sta but got:\n${logged}`
@@ -271,6 +276,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'interface-starts-with-i'"),
       `Expected a warning for interface-starts-with-i but got:\n${logged}`
@@ -294,6 +300,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'interface-starts-with-i'"),
       `Expected a warning for interface-starts-with-i but got:\n${logged}`
@@ -317,6 +324,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'reason-string'"),
       `Expected a warning for reason-string but got:\n${logged}`
@@ -340,6 +348,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'immutable-vars-naming'"),
       `Expected a warning for immutable-vars-naming but got:\n${logged}`
@@ -363,6 +372,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'immutable-vars-naming'"),
       `Expected a warning for immutable-vars-naming but got:\n${logged}`
@@ -386,6 +396,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'explicit-types'"),
       `Expected a warning for explicit-types but got:\n${logged}`
@@ -409,6 +420,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'quotes'"),
       `Expected a warning for quotes but got:\n${logged}`
@@ -432,6 +444,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'foundry-test-functions'"),
       `Expected a warning for foundry-test-functions but got:\n${logged}`
@@ -455,6 +468,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'foundry-test-functions'"),
       `Expected a warning for foundry-test-functions but got:\n${logged}`
@@ -478,6 +492,7 @@ describe('Better errors addition + rule disable on error', () => {
       .getCalls()
       .map((c) => c.args[0])
       .join('\n')
+
     assert.ok(
       logged.includes("invalid configuration for rule 'foundry-test-functions'"),
       `Expected a warning for foundry-test-functions but got:\n${logged}`
@@ -488,125 +503,81 @@ describe('Better errors addition + rule disable on error', () => {
     sinon.assert.notCalled(reportWarnSpy)
   })
 
-  // array with string as config
-  // array with number as config
-  // no array just severity
-  // no array, empty severity
-  // misspelled rule name
-  // additional config value in array
-  // config error object instead of array
-  // config error object with missing key
-  // config error object with no value type
-  // config error object with wrong value type
-
-  // it('Invalid CFG - not execute: reason-string when no property is defined)', () => {
-  //   const report = linter.processStr(dummyCode, {
-  //     rules: { 'interface-starts-with-i': { severity: 'warn' } },
-  //   })
-
-  //   assert.equal(report.errorCount, 0)
-  //   assert.equal(report.warningCount, 0)
-  //   assert.deepEqual(report.messages, [])
-
-  //   const logged = warnSpy
-  //     .getCalls()
-  //     .map((c) => c.args[0])
-  //     .join('\n')
-  //   assert.ok(
-  //     logged.includes("invalid configuration for rule 'interface-starts-with-i'"),
-  //     `Expected a warning for interface-starts-with-i but got:\n${logged}`
-  //   )
-
-  //   assert.ok(warnSpy.called, 'console.warn should have been called')
-  //   sinon.assert.notCalled(reportErrorSpy)
-  //   sinon.assert.notCalled(reportWarnSpy)
-  // })
-
-  // it('Invalid CFG - not execute: explicit-types expects two predefined values)', () => {
-  //   const code = contractWith('uint256 public constant SNAKE_CASE = 1;')
-
-  //   const report = linter.processStr(code, {
-  //     rules: { 'explicit-types': ['error', 'implicito'] },
-  //   })
-  //   assert.equal(report.errorCount, 0)
-  //   assert.equal(report.warningCount, 0)
-  //   assert.deepEqual(report.messages, [])
-  // })
-
-  // it('should run rule with valid config (compiler-version string)', () => {
-  //   const report = linter.processStr(dummyCode, {
-  //     rules: { 'compiler-version': ['error', '^0.8.24'] },
-  //   })
-  //   assert.equal(report.warningCount, 0)
-  //   assert.ok(
-  //     warnMessages.length === 0,
-  //     `Did not expect warnings, but got:\n${warnMessages.join('\n')}`
-  //   )
-  // })
-
-  // it('should not run rule when config is invalid (reason-string expects number)', () => {
-  //   const report = linter.processStr(dummyCode, {
-  //     rules: { 'reason-string': ['warn', { maxLength: 'not-a-number' }] },
-  //   })
-  //   assert.equal(report.errorCount, 0)
-  //   assert.equal(report.warningCount, 0)
-  //   assert.deepEqual(report.messages, [])
-  //   assert.ok(
-  //     findConfigWarning('reason-string'),
-  //     `Expected config warning for reason-string. Got:\n${warnMessages.join('\n')}`
-  //   )
-  //   assert.ok(
-  //     warnMessages.some((msg) => msg.includes('→ config[1].maxLength: should be integer')),
-  //     `Expected fallback error includes "→ config[1].maxLength: should be integer", got:\n${warnMessages.join(
-  //       '\n'
-  //     )}`
-  //   )
-  // })
-
-  // it('should run rule with valid config (reason-string with number)', () => {
-  //   const report = linter.processStr(dummyCode, {
-  //     rules: { 'reason-string': ['warn', { maxLength: 32 }] },
-  //   })
-  //   assert.equal(report.warningCount, 0)
-  //   assert.ok(
-  //     warnMessages.length === 0,
-  //     `Did not expect warnings, but got:\n${warnMessages.join('\n')}`
-  //   )
-  // })
-
-  // it('should run rule with primitive config (code-complexity with number)', () => {
-  //   const report = linter.processStr(
-  //     contractWith('function a(uint x) public { if (x == 1) { if (x > 2) {} } }'),
-  //     {
-  //       rules: { 'code-complexity': ['warn', 1] },
-  //     }
-  //   )
-  //   assert(report.warningCount >= 1)
-  //   assert(report.messages.some((i) => i.ruleId === 'code-complexity'))
-  // })
-
-  // it('should run rule with array config (import-path-check)', () => {
-  //   const report = linter.processStr("import '../X.sol';", {
-  //     rules: { 'import-path-check': ['warn', ['[~dependenciesPath]']] },
-  //   })
-  //   assert.equal(report.errorCount, 0)
-  //   assert(report.warningCount >= 0)
-  //   assert.ok(
-  //     warnMessages.length === 0,
-  //     `Did not expect warnings, but got:\n${warnMessages.join('\n')}`
-  //   )
-  // })
-
-  // it('should warn if rule level is invalid', () => {
-  //   const report = linter.processStr(dummyCode, {
-  //     rules: { 'no-console': 'maybe' },
-  //   })
-  //   assert.equal(report.errorCount, 0)
-  //   assert.equal(report.warningCount, 0)
-  //   assert.deepEqual(report.messages, [])
-  //   assert.ok(
-  //     warnMessages.some((msg) => msg.includes("rule 'no-console' level is 'maybe'")),
-  //     `Expected warning for invalid severity, got:\n${warnMessages.join('\n')}`
-  //   )
-  // })
+  it('Invalid CFG - use-natspec with wrong type for "enabled"', () => {
+    const config = {
+      title: {
+        enabled: 'yes', // should be boolean
+      },
+    }
+
+    const report = linter.processStr(dummyCode, {
+      rules: { 'use-natspec': ['warn', config] },
+    })
+
+    assert.equal(report.errorCount, 0)
+    assert.equal(report.warningCount, 0)
+    assert.deepEqual(report.messages, [])
+
+    const logged = warnSpy
+      .getCalls()
+      .map((c) => c.args[0])
+      .join('\n')
+
+    assert.ok(logged.includes("invalid configuration for rule 'use-natspec'"), logged)
+    assert.ok(warnSpy.called)
+    sinon.assert.notCalled(reportErrorSpy)
+    sinon.assert.notCalled(reportWarnSpy)
+  })
+
+  it('Invalid CFG - use-natspec with ignore as non-object', () => {
+    const config = {
+      title: {
+        enabled: true,
+        ignore: 'contractName', // should be an object
+      },
+    }
+
+    const report = linter.processStr(dummyCode, {
+      rules: { 'use-natspec': ['warn', config] },
+    })
+
+    assert.equal(report.errorCount, 0)
+    assert.equal(report.warningCount, 0)
+    assert.deepEqual(report.messages, [])
+
+    const logged = warnSpy
+      .getCalls()
+      .map((c) => c.args[0])
+      .join('\n')
+
+    assert.ok(logged.includes("invalid configuration for rule 'use-natspec'"), logged)
+    assert.ok(warnSpy.called)
+  })
+
+  it('Invalid CFG - use-natspec with ignore.contract not as array', () => {
+    const config = {
+      title: {
+        enabled: true,
+        ignore: {
+          contract: 'WrongFormat', // must be array
+        },
+      },
+    }
+
+    const report = linter.processStr(dummyCode, {
+      rules: { 'use-natspec': ['warn', config] },
+    })
+
+    assert.equal(report.errorCount, 0)
+    assert.equal(report.warningCount, 0)
+    assert.deepEqual(report.messages, [])
+
+    const logged = warnSpy
+      .getCalls()
+      .map((c) => c.args[0])
+      .join('\n')
+
+    assert.ok(logged.includes("invalid configuration for rule 'use-natspec'"))
+    assert.ok(warnSpy.called)
+  })
 })
diff --git a/test/rules/best-practices/use-natspec.js b/test/rules/best-practices/use-natspec.js
new file mode 100644
index 0000000..247525f
--- /dev/null
+++ b/test/rules/best-practices/use-natspec.js
@@ -0,0 +1,547 @@
+const assert = require('assert')
+const linter = require('../../../lib/index')
+const { assertNoErrors, assertErrorMessage, assertErrorCount } = require('../../common/asserts')
+
+describe('Linter - use-natspec', () => {
+  it('should detect missing @title and @author and @notice in contract', () => {
+    const code = `
+      contract MyContract {
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorCount(report, 3)
+    assert.ok(report.reports[0].message, "Missing @title tag in contract 'MyContract'")
+    assert.ok(report.reports[1].message, "Missing @author tag in contract 'MyContract'")
+    assert.ok(report.reports[2].message, "Missing @notice tag in contract 'MyContract'")
+  })
+
+  it('should not fail if @inheritdoc is present for public function', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice Some notice
+      contract C {
+        /// @inheritdoc D
+        function x() external {}
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should fail if @param names do not match Solidity param names', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice Some notice
+      contract C {
+        /// @notice test
+        /// @param wrongName description
+        function f(uint256 rightName) external {}
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(
+      report,
+      "Mismatch in @param names for function 'f'. Expected: [rightName], Found: [wrongName]"
+    )
+  })
+
+  it('should not validate names when parameters are unnamed', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice Some notice
+      contract C {
+        /// @notice test
+        /// @param description
+        function f(uint256) external {}
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should validate @return with unnamed return value', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice Some notice
+      contract C {
+        /// @notice test
+        /// @return description
+        function f() external pure returns (uint256) {
+          return 1;
+        }
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should error when return is named and not documented', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice Some notice
+      contract C {
+        /// @notice test
+        function f() external pure returns (uint256 val) {
+          return val;
+        }
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assert.ok(report.reports[0].message, "Missing @return tag in function 'f'")
+    assert.ok(
+      report.reports[1].message,
+      "Mismatch in @return names for function 'f'. Expected: [val], Found: []"
+    )
+  })
+
+  it('should handle multiple @param and @return correctly', () => {
+    const code = `
+      /// @title Example
+      /// @author User
+      /// @notice Does something
+      contract Test {
+        /// @notice Multi param
+        /// @param a First
+        /// @param b Second
+        /// @return x Description
+        /// @return y Description
+        function multi(uint a, uint b) external pure returns (uint x, uint y) {
+          return (a, b);
+        }
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should ignore variables that are not public', () => {
+    const code = `
+      /**
+       * @title Example
+       * @author User
+       * @notice Does something
+       */
+      contract MyContract {
+        uint256 internal secret;
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should require @notice for public state variables', () => {
+    const code = `
+      /**
+       * @title Example
+       * @author User
+       * @notice Does something
+       */
+      contract MyContract {
+        uint256 public value;
+      }
+    `
+
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(report, "Missing @notice tag in variable 'value'")
+  })
+
+  it('should fail when natspec @param tags does not match Solidity parameters', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice test
+      contract C {
+        /// @notice does something
+        /// @param a description
+        /// @param b description
+        function test(uint a) external {}
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(
+      report,
+      "Mismatch in @param names for function 'test'. Expected: [a], Found: [a, b]"
+    )
+  })
+
+  it('should fail when natspec @param tags quantity does not match Solidity parameters', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice test
+      contract C {
+        /// @notice does something
+        /// @param description
+        function test(uint, uint) external {}
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(
+      report,
+      "Mismatch in @param count for function 'test'. Expected: 2, Found: 1"
+    )
+  })
+
+  it('should pass when unnamed Solidity params match @param count', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice test
+      contract C {
+        /// @notice does something
+        /// @param description
+        function test(uint) external {}
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should handle return parameters correctly when natspec does not describe them', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice test
+      contract C {
+        /// @notice does something
+        /// @return amount1 description
+        function test() external pure returns (uint256 amount1, uint256 amount2) {
+          return (1, amount);
+        }
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(
+      report,
+      "Mismatch in @return names for function 'test'. Expected: [amount1, amount2], Found: [amount1]"
+    )
+  })
+
+  it('should fail when one named return is undocumented', () => {
+    const code = `
+      /// @title A
+      /// @author B
+      /// @notice test
+      contract C {
+        /// @notice does something
+        /// @return amount description
+        function test() external pure returns (uint256, uint256 amount) {
+          return (1, amount);
+        }
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertErrorMessage(
+      report,
+      "Mismatch in @return count for function 'test'. Expected: 2, Found: 1"
+    )
+  })
+
+  it('should ignore function if @inheritdoc is present', () => {
+    const code = `
+      /**
+       * @title Interface
+       * @author Me
+       * @notice Interface notice
+       */
+      interface I {
+        /// @notice Function x
+        /// @param p1 Parameter 1
+        function x() external;
+      }
+
+      /// @title Impl
+      /// @author Me
+      /// @notice Impl notice
+      contract C is I {
+        /// @inheritdoc I
+        function x() external {}
+      }
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assertNoErrors(report)
+  })
+
+  it('should require title/author/notice in library', () => {
+    const code = `
+      library MyLib {}
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assert.ok(report.reports[0].message, "Missing @title tag in library 'MyLib'")
+    assert.ok(report.reports[1].message, "Missing @author tag in library 'MyLib'")
+    assert.ok(report.reports[2].message, "Missing @notice tag in library 'MyLib'")
+  })
+
+  it('should require tags in interface', () => {
+    const code = `
+      interface MyInterface {}
+    `
+    const report = linter.processStr(code, {
+      rules: { 'use-natspec': 'error' },
+    })
+
+    assert.ok(report.reports[0].message, "Missing @title tag in interface 'MyInterface'")
+    assert.ok(report.reports[1].message, "Missing @author tag in interface 'MyInterface'")
+    assert.ok(report.reports[2].message, "Missing @notice tag in interface 'MyInterface'")
+  })
+
+  /// ///////////////////////////////////////////////////////////////
+  /// //////////////////// Configuration //////////////////////////////
+  /// ///////////////////////////////////////////////////////////////
+
+  describe('==> use-natspec - Configuration rule', () => {
+    it('should ignore @title for contract using per-type ignore list', () => {
+      const code = `
+                    contract IgnoreTitle {
+                    }
+                    `
+
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              title: {
+                enabled: true,
+                ignore: {
+                  contract: ['IgnoreTitle'],
+                },
+              },
+              author: {
+                enabled: true,
+              },
+              notice: {
+                enabled: true,
+              },
+            },
+          ],
+        },
+      })
+
+      assertErrorCount(report, 2)
+      assert.ok(report.reports[0].message, "Missing @author tag in contract 'IgnoreTitle'")
+      assert.ok(report.reports[1].message, "Missing @notice tag in contract 'IgnoreTitle'")
+    })
+
+    it('should ignore all tags globally for a contract and interface using "*" ignore', () => {
+      const code = `
+                    interface IgnoreAllTags {
+                    }
+                    contract LegacyContract {
+                    }
+                    `
+
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              title: { enabled: true, ignore: { '*': ['LegacyContract', 'IgnoreAllTags'] } },
+              author: { enabled: true, ignore: { '*': ['LegacyContract', 'IgnoreAllTags'] } },
+              notice: { enabled: true, ignore: { '*': ['LegacyContract', 'IgnoreAllTags'] } },
+            },
+          ],
+        },
+      })
+
+      assertNoErrors(report)
+    })
+
+    it('should ignore @notice tag for one variable using ignore.variable', () => {
+      const code = `
+                    /// @title Config Test
+                    /// @author User
+                    /// @notice This contract is for testing purposes
+                    contract ConfigTest {
+                      uint256 public ignoredVariable;
+                      uint256 public normalVariable;
+                    }
+                  `
+
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              notice: {
+                enabled: true,
+                ignore: {
+                  variable: ['ignoredVariable'],
+                },
+              },
+            },
+          ],
+        },
+      })
+
+      assertErrorCount(report, 1)
+      assertErrorMessage(report, "Missing @notice tag in variable 'normalVariable'")
+    })
+
+    it('should ignore @param for specific function using ignore.function', () => {
+      const code = `
+                    /**
+                     * @title Function Ignore Test
+                     * @author User
+                     * @notice This contract tests function-specific ignores
+                     */
+                    contract MyContract {
+                      /// @notice Some explanation
+                      function skipCheck(uint256 x) public {}
+                    }
+                  `
+
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              param: {
+                enabled: true,
+                ignore: {
+                  function: ['skipCheck'],
+                },
+              },
+            },
+          ],
+        },
+      })
+
+      assertNoErrors(report)
+    })
+
+    it('should ignore @return for specific function with named return', () => {
+      const code = `
+                    /**
+                     * @title Function Ignore Test
+                     * @author User
+                     * @notice This contract tests function-specific ignores
+                     */
+                    contract MyContract {
+                      /// @notice Something
+                      /// @param x input
+                      function getValue(uint x) public pure returns (uint256 result) {
+                        return x;
+                      }
+                    }
+                  `
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              return: {
+                enabled: true,
+                ignore: {
+                  function: ['getValue'],
+                },
+              },
+            },
+          ],
+        },
+      })
+
+      assertNoErrors(report)
+    })
+
+    it('should error on @author, ignore @title for specific contract and error on @return', () => {
+      const code = `
+                    contract HalfIgnored {
+
+                      function doSomething(uint256 amount) external returns(uint256) {}
+                    }
+                  `
+
+      const report = linter.processStr(code, {
+        rules: {
+          'use-natspec': [
+            'error',
+            {
+              title: {
+                enabled: true,
+                ignore: {
+                  contract: ['HalfIgnored'],
+                },
+              },
+              notice: {
+                enabled: false,
+              },
+              param: {
+                enabled: false,
+              },
+            },
+          ],
+        },
+      })
+
+      assertErrorCount(report, 3)
+      assert.ok(report.reports[0].message, "Missing @author tag in contract 'HalfIgnored'")
+      assert.ok(report.reports[1].message, "Missing @return tag in function 'doSomething'")
+      assert.ok(
+        report.reports[2].message,
+        "Mismatch in @return count for function 'doSomething'. Expected: 1, Found: 0"
+      )
+    })
+  })
+})
diff --git a/test/rules/gas-consumption/gas-named-return-values.js b/test/rules/gas-consumption/gas-named-return-values.js
index e08f924..4b603c8 100644
--- a/test/rules/gas-consumption/gas-named-return-values.js
+++ b/test/rules/gas-consumption/gas-named-return-values.js
@@ -76,6 +76,7 @@ describe('Linter - gas-named-return-values', () => {
         'compiler-version': 'off',
         'comprehensive-interface': 'off',
         'foundry-test-functions': 'off',
+        'use-natspec': 'off',
       },
     })
 
diff --git a/test/rules/naming/func-named-parameters.js b/test/rules/naming/func-named-parameters.js
index 4d700f7..a5f7504 100644
--- a/test/rules/naming/func-named-parameters.js
+++ b/test/rules/naming/func-named-parameters.js
@@ -71,6 +71,7 @@ describe('Linter - func-named-parameters', () => {
         'compiler-version': 'off',
         'comprehensive-interface': 'off',
         'foundry-test-functions': 'off',
+        'use-natspec': 'off',
       },
     })