Skip to content

scope: raw goes blind to real code in fragment files (.cpp/.h/.cxx/.qml) once any doc comment is present #1182

Description

@veshivas

Summary

A scope: raw rule goes blind to the rest of a fragment file (.cpp/.h/.cxx/.qml routed through [formats] to qdoc) as soon as that file contains any /*! ... */ doc comment. Without a doc comment, scope: raw correctly sees the whole file. With one, it sees only leftover state from the last doc comment processed. Real code before, between, and after doc comments becomes invisible to raw-scoped rules.

Root cause

lintFragments (internal/lint/fragment.go) loops over every doc comment found in the file and calls:

f.SetText(comment.Text)

SetText (internal/core/file.go) replaces the file's content on every call:

func (f *File) SetText(s string) {
	f.Content = s
	f.Lines = strings.SplitAfter(s, "\n")
	f.history = map[string]int{}
}

So each iteration of the loop replaces f.Lines/f.Content with just that one comment's text. After the loop finishes, only the last processed doc comment's text remains in f.Lines.

Back in lint.go, immediately after lintFragments returns:

// Run all rules with `scope: raw`
raw := nlp.NewBlock("", strings.Join(file.Lines, ""), "raw"+file.RealExt)
err = l.lintBlock(file, raw, len(file.Lines), 0, true)

This builds the scope: raw block from file.Lines as left by the loop above, not from the original file content. When the file has no doc comment, the loop never runs, so file.Lines still holds the untouched original file, and raw-scope rules behave normally on it. When the file has one or more doc comments, the loop clobbers file.Lines down to just the last one's text.

The behavior of scope: raw depends on something with no relationship to a rule's own scope: whether the file happens to contain a /*! ... */ doc comment. Two .cpp files with identical real code get two different outcomes for the same rule, based only on that.

A temporary debug print placed right before the raw := nlp.NewBlock(...) line in lint.go, logging file.Content at that exact point, confirms this directly:

# with_doc_comment.cpp (has one doc comment)
file.Content="\n\\fn void Widget::clicked()\n\nOrdinary prose with no marker here.\n\n"

# no_doc_comment.cpp (no doc comment)
file.Content="#include \"widget.h\"\n\nint ZQX_marker_one() { return 0; }\n\nvoid Widget::other() {}\n"

By that point, the first file's surrounding code is missing. The second file's code is intact. Whichever outcome is correct for a rule, scope: raw shouldn't flip between them based on unrelated file content.

Reproduction

Built from v3 (commit 520962ef). Both files use an identical ZQX marker in identical real code at an identical line and column. The only difference is an unrelated doc comment appearing later in the second file:

.vale.ini

StylesPath = styles
MinAlertLevel = suggestion

[formats]
cpp = qdoc

[*.cpp]
T.Tok = YES

styles/T/Tok.yml

extends: existence
message: "'%s' found"
level: error
scope: raw
nonword: true
raw:
  - ZQX

no_doc_comment.cpp

#include "widget.h"

int ZQX_marker_one() { return 0; }

void Widget::other() {}

with_doc_comment.cpp

#include "widget.h"

int ZQX_marker_one() { return 0; }

/*!
    \fn void Widget::clicked()

    Ordinary prose with no marker here.
*/

void Widget::other() {}
$ vale --config=.vale.ini no_doc_comment.cpp
no_doc_comment.cpp:3:5:T.Tok:'ZQX' found
$ echo $?
1

$ vale --config=.vale.ini with_doc_comment.cpp
$ echo $?
0

with_doc_comment.cpp should report the same ZQX finding at 3:5: the marker is at an identical position in identical, unrelated code. It doesn't, because the unrelated doc comment elsewhere in the file clobbers f.Lines before the scope: raw block gets built.

Test case

Added as lint/qdoc-fragments-raw-scope in testdata/e2e/lint.yaml, asserting the correct (currently failing) expectation that both files report identically. Confirmed it's the only failing case in the existing go test ./internal/e2e/... suite. No other regressions appear.

--- FAIL: TestScenarios/lint/qdoc-fragments-raw-scope (0.03s)
    code_no_comment.cpp:3:5:T.Tok:'ZQX' found
  - code_with_comment.cpp:3:5:T.Tok:'ZQX' found
Patch: testdata/e2e/lint.yaml addition (expand)
diff --git a/testdata/e2e/lint.yaml b/testdata/e2e/lint.yaml
index 11fcb5a4..1f3b34ab 100644
--- a/testdata/e2e/lint.yaml
+++ b/testdata/e2e/lint.yaml
@@ -753,6 +753,60 @@ cases:
       widget.cpp:6:23:T.Tok:'ZQX' in a doc comment
       widget.cpp:8:26:T.Tok:'ZQX' in a doc comment
 
+  - name: qdoc-fragments-raw-scope
+    about: "BUG: a `scope: raw` rule sees the whole file when a fragment file
+      (here, `.cpp` via `[formats]`) has no doc comment at all, but goes blind
+      to everything outside the doc comment once one is present. lintFragments
+      calls f.SetText(comment.Text) once per `/*! ... */` block found
+      (internal/lint/fragment.go), which overwrites f.Lines/f.Content rather
+      than restoring them; lint.go then builds the `scope: raw` block from
+      whatever f.Lines was left holding after that loop, not the real file.
+      code_no_comment.cpp and code_with_comment.cpp carry the identical `ZQX`
+      marker in identical real code at an identical position -- the only
+      difference is an unrelated doc comment appearing later in the second
+      file. A correct `scope: raw` rule should report both identically."
+    files:
+      .vale.ini: |
+        StylesPath = styles
+        MinAlertLevel = suggestion
+
+        [formats]
+        cpp = qdoc
+
+        [*.cpp]
+        T.Tok = YES
+      styles/T/Tok.yml: |
+        extends: existence
+        message: "'%s' found"
+        level: error
+        scope: raw
+        nonword: true
+        raw:
+          - ZQX
+      code_no_comment.cpp: |
+        #include "widget.h"
+
+        int ZQX_marker_one() { return 0; }
+
+        void Widget::other() {}
+      code_with_comment.cpp: |
+        #include "widget.h"
+
+        int ZQX_marker_one() { return 0; }
+
+        /*!
+            \fn void Widget::clicked()
+
+            Ordinary prose with no marker here.
+        */
+
+        void Widget::other() {}
+    args: .
+    exit: 1
+    want: |
+      code_no_comment.cpp:3:5:T.Tok:'ZQX' found
+      code_with_comment.cpp:3:5:T.Tok:'ZQX' found
+
   - name: qdocinc-include
     about: "a `.qdocinc` is an include, not a file of comments: its markup is
       the body of one, so nothing waits for a `/*!` that the file never

Impact

This affects any scope: raw rule applied to .cpp/.h/.cxx/.qml-mapped fragment formats, including built-in extends: script/existence rules such as line-length checks. Real code becomes unreachable by raw-scope rules the moment the file contains a doc comment. An otherwise identical file with no doc comment gets scanned in full. This inconsistency produces false negatives, as shown here, or false positives when a doc-comment-free file gets linted as if all its code were prose.

Suggested fix direction

lintFragments shouldn't leave f.Lines/f.Content in a per-comment clobbered state after it returns. The simplest fix restores f.Lines/f.Content to the original file once the loop over doc comments completes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions