diff --git a/.README/README.md b/.README/README.md
index 31487f62..bb122ae0 100644
--- a/.README/README.md
+++ b/.README/README.md
@@ -247,6 +247,7 @@ non-default-recommended fixer).
|:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content|
|:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)|
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
+|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks|
|||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions|
||:wrench:|[match-name](./docs/rules/match-name.md#readme)|Reports the name portion of a JSDoc tag if matching or not matching a given regular expression|
|:heavy_check_mark:|:wrench:|[multiline-blocks](./docs/rules/multiline-blocks.md#readme)|Controls how and whether jsdoc blocks can be expressed as single or multiple line blocks|
diff --git a/.README/rules/lines-before-block.md b/.README/rules/lines-before-block.md
new file mode 100644
index 00000000..441cb2c8
--- /dev/null
+++ b/.README/rules/lines-before-block.md
@@ -0,0 +1,37 @@
+# `lines-before-block`
+
+This rule enforces minimum number of newlines before JSDoc comment blocks
+(except at the beginning of a file).
+
+## Options
+
+### `lines`
+
+The minimum number of lines to require. Defaults to 1.
+
+### `ignoreSameLine`
+
+This option excludes cases where the JSDoc block occurs on the same line as a
+preceding code or comment. Defaults to `true`.
+
+### `excludedTags`
+
+An array of tags whose presence in the JSDoc block will prevent the
+application of the rule. Defaults to `['type']` (i.e., if `@type` is present,
+lines before the block will not be added).
+
+|||
+|---|---|
+|Context|everywhere|
+|Tags|N/A|
+|Recommended|true|
+|Settings||
+|Options|`excludedTags`, `ignoreSameLine`, `lines`|
+
+## Failing examples
+
+
+
+## Passing examples
+
+
diff --git a/README.md b/README.md
index a9dae4cb..09975474 100644
--- a/README.md
+++ b/README.md
@@ -277,6 +277,7 @@ non-default-recommended fixer).
|:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content|
|:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)|
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
+|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks|
|||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions|
||:wrench:|[match-name](./docs/rules/match-name.md#readme)|Reports the name portion of a JSDoc tag if matching or not matching a given regular expression|
|:heavy_check_mark:|:wrench:|[multiline-blocks](./docs/rules/multiline-blocks.md#readme)|Controls how and whether jsdoc blocks can be expressed as single or multiple line blocks|
diff --git a/docs/rules/lines-before-block.md b/docs/rules/lines-before-block.md
new file mode 100644
index 00000000..49ef97d2
--- /dev/null
+++ b/docs/rules/lines-before-block.md
@@ -0,0 +1,150 @@
+
+
+# lines-before-block
+
+This rule enforces minimum number of newlines before JSDoc comment blocks
+(except at the beginning of a file).
+
+
+
+## Options
+
+
+
+### lines
+
+The minimum number of lines to require. Defaults to 1.
+
+
+
+### ignoreSameLine
+
+This option excludes cases where the JSDoc block occurs on the same line as a
+preceding code or comment. Defaults to `true`.
+
+
+
+### excludedTags
+
+An array of tags whose presence in the JSDoc block will prevent the
+application of the rule. Defaults to `['type']` (i.e., if `@type` is present,
+lines before the block will not be added).
+
+|||
+|---|---|
+|Context|everywhere|
+|Tags|N/A|
+|Recommended|true|
+|Settings||
+|Options|`excludedTags`, `ignoreSameLine`, `lines`|
+
+
+
+## Failing examples
+
+The following patterns are considered problems:
+
+````js
+someCode;
+/**
+ *
+ */
+// Message: Required 1 line(s) before JSDoc block
+
+someCode; /**
+ *
+ */
+// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}]
+// Message: Required 1 line(s) before JSDoc block
+
+someCode; /** */
+// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}]
+// Message: Required 1 line(s) before JSDoc block
+
+someCode;
+/**
+ *
+ */
+// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}]
+// Message: Required 2 line(s) before JSDoc block
+
+// Some comment
+/**
+ *
+ */
+// Message: Required 1 line(s) before JSDoc block
+
+/* Some comment */
+/**
+ *
+ */
+// Message: Required 1 line(s) before JSDoc block
+
+/**
+ * Some comment
+ */
+/**
+ *
+ */
+// Message: Required 1 line(s) before JSDoc block
+````
+
+
+
+
+
+## Passing examples
+
+The following patterns are not considered problems:
+
+````js
+/**
+*
+*/
+
+someCode;
+
+/**
+ *
+ */
+
+someCode;
+
+
+/**
+ *
+ */
+// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}]
+
+// Some comment
+
+/**
+ *
+ */
+
+/* Some comment */
+
+/**
+ *
+ */
+
+/**
+ * Some comment
+ */
+
+/**
+ *
+ */
+
+someCode; /** */
+
+const a = {
+ someProp: /** @type {SomeCast} */ (someVal)
+};
+
+const a = /** @lends SomeClass */ {
+ someProp: (someVal)
+};
+// "jsdoc/lines-before-block": ["error"|"warn", {"excludedTags":["lends"],"ignoreSameLine":false}]
+````
+
diff --git a/src/index.js b/src/index.js
index b01eacd3..61c6fe76 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,6 +15,7 @@ import emptyTags from './rules/emptyTags.js';
import implementsOnClasses from './rules/implementsOnClasses.js';
import importsAsDependencies from './rules/importsAsDependencies.js';
import informativeDocs from './rules/informativeDocs.js';
+import linesBeforeBlock from './rules/linesBeforeBlock.js';
import matchDescription from './rules/matchDescription.js';
import matchName from './rules/matchName.js';
import multilineBlocks from './rules/multilineBlocks.js';
@@ -92,6 +93,7 @@ const index = {
'implements-on-classes': implementsOnClasses,
'imports-as-dependencies': importsAsDependencies,
'informative-docs': informativeDocs,
+ 'lines-before-block': linesBeforeBlock,
'match-description': matchDescription,
'match-name': matchName,
'multiline-blocks': multilineBlocks,
@@ -167,6 +169,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
'jsdoc/implements-on-classes': warnOrError,
'jsdoc/imports-as-dependencies': 'off',
'jsdoc/informative-docs': 'off',
+ 'jsdoc/lines-before-block': warnOrError,
'jsdoc/match-description': 'off',
'jsdoc/match-name': 'off',
'jsdoc/multiline-blocks': warnOrError,
diff --git a/src/rules/linesBeforeBlock.js b/src/rules/linesBeforeBlock.js
new file mode 100644
index 00000000..086193c2
--- /dev/null
+++ b/src/rules/linesBeforeBlock.js
@@ -0,0 +1,101 @@
+import iterateJsdoc from '../iterateJsdoc.js';
+
+export default iterateJsdoc(({
+ context,
+ jsdocNode,
+ sourceCode,
+ report,
+ utils,
+}) => {
+ const {
+ lines = 1,
+ ignoreSameLine = true,
+ excludedTags = ['type']
+ } = context.options[0] || {};
+
+ if (utils.hasATag(excludedTags)) {
+ return;
+ }
+
+ const tokensBefore = sourceCode.getTokensBefore(jsdocNode, {includeComments: true});
+ const tokenBefore = tokensBefore.slice(-1)[0];
+ if (!tokenBefore) {
+ return;
+ }
+
+ if (tokenBefore.loc?.end?.line + lines >=
+ /** @type {number} */
+ (jsdocNode.loc?.start?.line)
+ ) {
+ const startLine = jsdocNode.loc?.start?.line;
+ const sameLine = tokenBefore.loc?.end?.line === startLine;
+
+ if (sameLine && ignoreSameLine) {
+ return;
+ }
+
+ /** @type {import('eslint').Rule.ReportFixer} */
+ const fix = (fixer) => {
+ let indent = '';
+ if (sameLine) {
+ const spaceDiff = /** @type {number} */ (jsdocNode.loc?.start?.column) -
+ /** @type {number} */ (tokenBefore.loc?.end?.column);
+ // @ts-expect-error Should be a comment
+ indent = /** @type {import('estree').Comment} */ (
+ jsdocNode
+ ).value.match(/^\*\n([ \t]*) \*/)?.[1]?.slice(spaceDiff);
+ if (!indent) {
+ /** @type {import('eslint').AST.Token|import('estree').Comment|undefined} */
+ let tokenPrior = tokenBefore;
+ let startColumn;
+ while (tokenPrior && tokenPrior?.loc?.start?.line === startLine) {
+ startColumn = tokenPrior.loc?.start?.column;
+ tokenPrior = tokensBefore.pop();
+ }
+ indent = ' '.repeat(
+ /* c8 ignore next */
+ /** @type {number} */ (startColumn ? startColumn - 1 : 0)
+ );
+ }
+ }
+
+ return fixer.insertTextAfter(
+ /** @type {import('eslint').AST.Token} */
+ (tokenBefore),
+ '\n'.repeat(lines) +
+ (sameLine ? '\n' + indent : '')
+ );
+ };
+ report(`Required ${lines} line(s) before JSDoc block`, fix);
+ }
+}, {
+ iterateAllJsdocs: true,
+ meta: {
+ fixable: 'code',
+ docs: {
+ description: 'Enforces minimum number of newlines before JSDoc comment blocks',
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/lines-before-block.md#repos-sticky-header',
+ },
+ schema: [
+ {
+ additionalProperties: false,
+ properties: {
+ excludedTags: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ },
+ ignoreSameLine: {
+ type: 'boolean',
+ },
+ lines: {
+ type: 'integer'
+ }
+ },
+ type: 'object',
+ },
+ ],
+ type: 'suggestion',
+ },
+});
diff --git a/test/rules/assertions/linesBeforeBlock.js b/test/rules/assertions/linesBeforeBlock.js
new file mode 100644
index 00000000..51132b20
--- /dev/null
+++ b/test/rules/assertions/linesBeforeBlock.js
@@ -0,0 +1,246 @@
+export default {
+ invalid: [
+ {
+ code: `
+ someCode;
+ /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ output: `
+ someCode;
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ someCode; /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 2,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ options: [
+ {
+ ignoreSameLine: false
+ }
+ ],
+ output: `
+ someCode;
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ someCode; /** */
+ `,
+ errors: [
+ {
+ line: 2,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ options: [
+ {
+ ignoreSameLine: false
+ }
+ ],
+ output: `
+ someCode;
+
+ /** */
+ `,
+ },
+ {
+ code: `
+ someCode;
+ /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'Required 2 line(s) before JSDoc block',
+ },
+ ],
+ options: [
+ {
+ lines: 2,
+ },
+ ],
+ output: `
+ someCode;
+
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ // Some comment
+ /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ output: `
+ // Some comment
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ /* Some comment */
+ /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ output: `
+ /* Some comment */
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ /**
+ * Some comment
+ */
+ /**
+ *
+ */
+ `,
+ errors: [
+ {
+ line: 5,
+ message: 'Required 1 line(s) before JSDoc block',
+ },
+ ],
+ output: `
+ /**
+ * Some comment
+ */
+
+ /**
+ *
+ */
+ `,
+ },
+ ],
+ valid: [
+ {
+ code: `/**\n *\n */`,
+ },
+ {
+ code: `
+ someCode;
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ someCode;
+
+
+ /**
+ *
+ */
+ `,
+ options: [
+ {
+ lines: 2,
+ },
+ ],
+ },
+ {
+ code: `
+ // Some comment
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ /* Some comment */
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ /**
+ * Some comment
+ */
+
+ /**
+ *
+ */
+ `,
+ },
+ {
+ code: `
+ someCode; /** */
+ `,
+ },
+ {
+ code: `const a = {
+ someProp: /** @type {SomeCast} */ (someVal)
+ };
+ `,
+ },
+ {
+ code: `const a = /** @lends SomeClass */ {
+ someProp: (someVal)
+ };
+ `,
+ options: [
+ {
+ excludedTags: ['lends'],
+ ignoreSameLine: false
+ }
+ ]
+ },
+ ],
+};
diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json
index 1540d80b..df037553 100644
--- a/test/rules/ruleNames.json
+++ b/test/rules/ruleNames.json
@@ -16,6 +16,7 @@
"implements-on-classes",
"imports-as-dependencies",
"informative-docs",
+ "lines-before-block",
"match-description",
"match-name",
"multiline-blocks",