Skip to content

feat: add checkSiblingsOnly option to no-duplicate-headings rule #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

xbinaryx
Copy link

Prerequisites checklist

What is the purpose of this pull request?

This PR adds a new siblingsOnly option to the no-duplicate-headings rule to make it more flexible. Currently, the rule flags any duplicate headings in the entire document, which can be too restrictive in some cases. With this new option, users can configure the rule to only check for duplicate headings among siblings (headings at the same level under the same parent heading).

What changes did you make? (Give an overview)

Added a new siblingsOnly boolean option to the rule's schema and refactored the implementation to track headings by level, resetting tracking when moving between levels when siblingsOnly is enabled. Updated documentation and added test cases for both ATX and setext-style headings to verify the new behavior.

Related Issues

Fixes #390

Is there anything you'd like reviewers to focus on?

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new siblingsOnly option to the no-duplicate-headings rule so that duplicate heading checks may be applied only among headings that share the same parent.

  • Added a siblingsOnly boolean option in the rule’s schema and updated the implementation to track headings by level.
  • Updated tests and documentation to verify the behavior for ATX and setext-style headings.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
tests/rules/no-duplicate-headings.test.js New test cases validating the siblingsOnly behavior for both valid and invalid heading structures.
src/rules/no-duplicate-headings.js Modified duplicate heading tracking logic to support the siblingsOnly option using level-based tracking instead of a global set.
docs/rules/no-duplicate-headings.md Updated documentation with options and examples for the new siblingsOnly setting.

if (headings.has(text)) {
const headingText = getHeadingText(node);

if (siblingsOnly) {
Copy link
Preview

Copilot AI May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When siblingsOnly is false the rule should check for duplicate headings across the entire document. However, the current implementation always uses the level 1 array (currentLevelHeadings) to track headings, which can cause duplicate headings from different levels to be missed. Consider adding a separate global tracking set for the non-siblingsOnly case.

Copilot uses AI. Check for mistakes.

@github-project-automation github-project-automation bot moved this to Needs Triage in Triage May 31, 2025
@lumirlumir lumirlumir moved this from Needs Triage to Implementing in Triage May 31, 2025
@xbinaryx xbinaryx changed the title feat: add siblingsOnly option to no-duplicate-headings rule feat: add checkSiblingsOnly option to no-duplicate-headings rule Jun 2, 2025
@lumirlumir lumirlumir self-requested a review June 3, 2025 05:15
Copy link
Member

@lumirlumir lumirlumir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this issue. I've left some comments.

Comment on lines +64 to +85
function getHeadingText(node) {
/*
* There are two types of headings in markdown:
* - ATX headings, which start with one or more # characters
* - Setext headings, which are underlined with = or -
* Setext headings are identified by being on two lines instead of one,
* with the second line containing only = or - characters. In order to
* get the correct heading text, we need to determine which type of
* heading we're dealing with.
*/
const isSetext =
node.position.start.line !== node.position.end.line;

return isSetext
? // get only the text from the first line
sourceCode.lines[node.position.start.line - 1].trim()
: // get the text without the leading # characters
sourceCode
.getText(node)
.slice(node.depth + 1)
.trim();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add logic to handle closed ATX headings and add corresponding test cases?

Currently, getHeadingText gives a false negative when using closed-style ATX headings.

For example:

## installation

## installation ##

or

## installation

## installation ##########

The second one is an invalid case.

https://spec.commonmark.org/0.31.2/#atx-headings

image

if (checkSiblingsOnly) {
const currentLevel = node.depth;

const direction = currentLevel > lastLevel ? 1 : -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const direction = currentLevel > lastLevel ? 1 : -1;
const direction = currentLevel > lastLevel ? 1 : -1;

Could you give a name to the magic numbers 1 and -1?

Currently, it's somewhat unclear what these numbers are doing.

Maybe we can use a constant like this:

const DOWN = -1;
const UP = 1;

Or, we can use more concrete one with symbols:

const DOWN = Symbol('down');
const UP = Symbol('up'):


const direction = currentLevel > lastLevel ? 1 : -1;
while (lastLevel !== currentLevel) {
if (direction > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (direction > 0) {
if (direction > 0) {

Like the above comment, could you clarify the conditional statement?

Maybe we can use if (direction === UP) or something similar.

Comment on lines +54 to +55
? new Array(7).fill(null).map(() => new Set())
: [null, new Set()];
Copy link
Member

@lumirlumir lumirlumir Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? new Array(7).fill(null).map(() => new Set())
: [null, new Set()];
? new Array(7).fill(null).map(() => new Set())
: [null, new Set()];

Could you use a Map instead of an Array?

Currently, the leading null item in the array is unused and it causes some confusion.

Maybe we can use a Map like this:

new Map([
  [1, new Set()],
  [2, new Set()],
  [3, new Set()],
  [4, new Set()],
  [5, new Set()],
  [6, new Set()]
])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Implementing
Development

Successfully merging this pull request may close these issues.

Rule Change: add siblingsOnly option to no-duplicate-headings rule
3 participants