Skip to content

Commit 5ae4867

Browse files
committed
fix: read only \/*!\ as QDoc, and cover commands
Signed-off-by: Joseph Kato <joseph@jdkato.io>
1 parent e60b5bc commit 5ae4867

5 files changed

Lines changed: 164 additions & 20 deletions

File tree

‎internal/lint/fragment.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ func (l *Linter) lintFragments(f *core.File) error {
101101

102102
last := 0
103103
for _, comment := range comments {
104-
// QDoc is written in block comments; a `//` comment is code, not
105-
// documentation.
106-
if f.NormedExt == ".qdoc" && !strings.HasPrefix(comment.Source, "/*") {
104+
// QDoc reads `/*! ... */` only; a `//` line comment or a plain
105+
// `/* ... */` block is code, not documentation.
106+
if f.NormedExt == ".qdoc" && !strings.HasPrefix(comment.Source, "/*!") {
107107
continue
108108
}
109109

‎internal/lint/qdoc.go‎

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,31 @@ var qdocVerbatim = map[string]struct{}{
2727
"raw": {},
2828
}
2929

30-
// qdocSkipLine names the topic, context, and quoting commands whose whole
31-
// line is markup: identifiers, file paths, and cross-references, not prose.
30+
// qdocSkipLine names the topic, context, quoting, build-system, and
31+
// conditional commands whose whole line is markup: identifiers, file paths,
32+
// cross-references, and expressions, not prose.
3233
var qdocSkipLine = map[string]struct{}{
3334
"annotatedlist": {},
3435
"class": {},
36+
"cmakecomponent": {},
37+
"cmakepackage": {},
38+
"cmaketargetitem": {},
3539
"codeline": {},
40+
"compareswith": {},
3641
"contentspage": {},
42+
"dontdocument": {},
3743
"dots": {},
44+
"else": {},
45+
"endcompareswith": {},
46+
"endif": {},
3847
"enum": {},
3948
"example": {},
4049
"externalpage": {},
4150
"fn": {},
4251
"generatelist": {},
4352
"group": {},
4453
"headerfile": {},
54+
"if": {},
4555
"include": {},
4656
"indexpage": {},
4757
"ingroup": {},
@@ -55,6 +65,7 @@ var qdocSkipLine = map[string]struct{}{
5565
"meta": {},
5666
"module": {},
5767
"namespace": {},
68+
"nativetype": {},
5869
"nextpage": {},
5970
"noautolist": {},
6071
"nonreentrant": {},
@@ -69,12 +80,17 @@ var qdocSkipLine = map[string]struct{}{
6980
"qmlattachedsignal": {},
7081
"qmlbasictype": {},
7182
"qmlclass": {},
83+
"qmlenum": {},
7284
"qmlmethod": {},
7385
"qmlmodule": {},
7486
"qmlproperty": {},
7587
"qmlsignal": {},
88+
"qmlsingletontype": {},
7689
"qmltype": {},
7790
"qmlvaluetype": {},
91+
"qtcmakepackage": {},
92+
"qtcmaketargetitem": {},
93+
"qtvariable": {},
7894
"quotefile": {},
7995
"quotefromfile": {},
8096
"reentrant": {},
@@ -88,8 +104,10 @@ var qdocSkipLine = map[string]struct{}{
88104
"skipuntil": {},
89105
"snippet": {},
90106
"startpage": {},
107+
"tableofcontents": {},
91108
"target": {},
92109
"threadsafe": {},
110+
"typealias": {},
93111
"typedef": {},
94112
"variable": {},
95113
"wrapper": {},
@@ -109,9 +127,11 @@ var (
109127
qdocBlockCmd = regexp.MustCompile(`^\s*\\([a-zA-Z0-9]+)\s*(.*)$`)
110128
// An inline command and its argument: `\c word` or `\c {some words}`,
111129
// with an optional [qualifier] for `\l`. A braced argument may wrap
112-
// within its paragraph.
113-
qdocInlineCmd = regexp.MustCompile(`\\([a-zA-Z0-9]+)\s*(\[[^\]\n]*\]\s*)?(\{[^{}]*\}|[^\s{][^\s]*)?`)
114-
// The braced {text} that may follow a braced `\l` target.
130+
// within its paragraph. `\\` is an escaped backslash, matched first so
131+
// that the command that follows one is read as text.
132+
qdocInlineCmd = regexp.MustCompile(`\\\\|\\([a-zA-Z0-9]+)\s*(\[[^\]\n]*\]\s*)?(\{[^{}]*\}|[^\s{][^\s]*)?`)
133+
// The braced {text} that may follow a braced `\l` target or `\span`
134+
// attribute.
115135
qdocLinkText = regexp.MustCompile(`^\s*\{[^{}]*\}`)
116136
)
117137

@@ -144,6 +164,13 @@ func qdocInline(text string) string {
144164
}
145165
out.WriteString(text[:loc[0]])
146166

167+
if loc[2] < 0 {
168+
// `\\` is a literal backslash, not a command.
169+
out.WriteString(`\`)
170+
text = text[loc[1]:]
171+
continue
172+
}
173+
147174
name := text[loc[2]:loc[3]]
148175
arg := ""
149176
if loc[6] >= 0 {
@@ -171,9 +198,20 @@ func qdocInline(text string) string {
171198
}
172199
}
173200
out.WriteString(`<a href="#">` + label + "</a>")
201+
case "span":
202+
// `\span {class="x"} {text}`: the attribute is markup, the
203+
// braced text that follows is prose.
204+
label := ""
205+
if m := qdocLinkText.FindString(rest); m != "" {
206+
label = qdocArg(strings.TrimSpace(m))
207+
rest = rest[len(m):]
208+
}
209+
out.WriteString("<span>" + label + "</span>")
174210
case "image", "inlineimage":
175211
// The argument is a file name; a caption, if any, follows as
176212
// ordinary prose.
213+
case "unicode":
214+
// The argument is a code point, not prose.
177215
default:
178216
// An unknown command is markup; whatever followed it is prose.
179217
rest = text[loc[3]:]
@@ -270,14 +308,24 @@ func (c *qdocConv) endItem() {
270308
top.inItem = false
271309
}
272310

311+
// qdocBlankDelims blanks the comment delimiters of a standalone .qdoc file,
312+
// which a command or its prose may share a line with: `/*! \page index.html`.
313+
// They are replaced by spaces rather than removed so that a column in the
314+
// converted line is still a column in the source.
315+
func qdocBlankDelims(raw string) string {
316+
if i := strings.Index(raw, "/*!"); i >= 0 && strings.TrimSpace(raw[:i]) == "" {
317+
raw = raw[:i] + " " + raw[i+3:]
318+
}
319+
if i := strings.LastIndex(raw, "*/"); i >= 0 && strings.TrimSpace(raw[i+2:]) == "" {
320+
raw = raw[:i] + " " + raw[i+2:]
321+
}
322+
return raw
323+
}
324+
273325
func (c *qdocConv) line(raw string) { //nolint:gocyclo // one case per command family
326+
raw = qdocBlankDelims(raw)
274327
trimmed := strings.TrimSpace(raw)
275328

276-
// Comment delimiters, in both standalone .qdoc files and sources.
277-
if trimmed == "/*!" || trimmed == "*/" || strings.HasPrefix(trimmed, "/*!") && strings.HasSuffix(trimmed, "*/") {
278-
return
279-
}
280-
281329
if c.verbatim != "" {
282330
if m := qdocBlockCmd.FindStringSubmatch(raw); m != nil && m[1] == c.verbatim {
283331
c.verbatim = ""

‎internal/lint/qdoc_test.go‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,65 @@ func TestQdocHTML(t *testing.T) {
114114
[]string{"The rest of the sentence."},
115115
[]string{"unknowncmd"},
116116
},
117+
{
118+
"a command may share the opening delimiter's line",
119+
"/*! \\page overview.html\n \\title The Page Title\n*/\n",
120+
[]string{"<h1>The Page Title</h1>"},
121+
[]string{"/*!", "*/", "overview"},
122+
},
123+
{
124+
"prose may share the opening delimiter's line",
125+
"/*! One brief sentence. */\n",
126+
[]string{"One brief sentence."},
127+
[]string{"/*!", "*/"},
128+
},
129+
{
130+
"adjacent comment blocks are separate paragraphs",
131+
"/*!\n First block.\n*/\n\n/*!\n Second block.\n*/\n",
132+
[]string{"<p> First block.</p>", "<p> Second block.</p>"},
133+
nil,
134+
},
135+
{
136+
"build-system and type commands are markup",
137+
"\\qmlenum Mode\n\\qmlsingletontype Theme\n\\typealias Alias\n" +
138+
"\\nativetype QString\n\\cmakepackage Qt6\n\\cmakecomponent Widgets\n" +
139+
"\\cmaketargetitem Target\n\\qtcmakepackage Core\n" +
140+
"\\qtcmaketargetitem Item\n\\qtvariable widgets\n" +
141+
"\\tableofcontents auto\n\\dontdocument (Hidden)\n\nProse here.\n",
142+
[]string{"<p>Prose here.</p>"},
143+
[]string{"Mode", "Theme", "Alias", "QString", "Qt6", "Widgets",
144+
"Target", "Core", "Item", "widgets", "auto", "Hidden"},
145+
},
146+
{
147+
"a comparison block's command lines are markup",
148+
"\\compareswith equality QAnyStringView\n\\endcompareswith\n\nProse here.\n",
149+
[]string{"<p>Prose here.</p>"},
150+
[]string{"QAnyStringView"},
151+
},
152+
{
153+
"a conditional's expression is markup, its branches prose",
154+
"\\if defined(onlinedocs)\nOnline prose.\n\\else\nOffline prose.\n\\endif\n",
155+
[]string{"Online prose.", "Offline prose."},
156+
[]string{"onlinedocs"},
157+
},
158+
{
159+
"an escaped backslash is text, not a command",
160+
"Write \\\\c to show the command.\n",
161+
[]string{`Write \c to show the command.`},
162+
[]string{"<code>"},
163+
},
164+
{
165+
"a unicode code point is markup",
166+
"A bullet \\unicode 0x2022 sits here.\n",
167+
[]string{"A bullet", "sits here."},
168+
[]string{"0x2022"},
169+
},
170+
{
171+
"a span's attribute is markup, its text prose",
172+
"See \\span {class=\"vrheader\"} {the spanned words} here.\n",
173+
[]string{"<span>the spanned words</span>", "here."},
174+
[]string{"vrheader", "class="},
175+
},
117176
}
118177

119178
for _, c := range cases {

‎testdata/e2e/lint.yaml‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,9 @@ cases:
348348
- name: qdoc
349349
about: "#784 -- QDoc's commands are markup: topic lines, code blocks,
350350
links' targets, and options say nothing, while titles, sections,
351-
briefs, notes, items, cells, and captions are prose"
351+
briefs, notes, items, cells, and captions are prose. A command may
352+
share the opening `/*!` line, and a conditional block's prose still
353+
counts"
352354
args: test.qdoc
353355
exit: 0
354356
want: |
@@ -364,14 +366,18 @@ cases:
364366
test.qdoc:33:36:vale.Annotations:'TODO' left in text
365367
test.qdoc:36:46:vale.Annotations:'XXX' left in text
366368
test.qdoc:38:47:vale.Annotations:'TODO' left in text
369+
test.qdoc:48:34:vale.Annotations:'TODO' left in text
370+
test.qdoc:67:37:vale.Annotations:'XXX' left in text
371+
test.qdoc:69:27:vale.Annotations:'TODO' left in text
372+
test.qdoc:73:61:vale.Annotations:'XXX' left in text
367373
absent:
368374
- FIXME
369375
- NOTE
370376

371377
- name: qdoc-fragments
372378
about: doc comments in C++ sources lint as QDoc through `[formats]`, with
373-
alerts mapped back to the source; block comments are documentation,
374-
line comments are code
379+
alerts mapped back to the source; only `/*!` is documentation, while
380+
line comments and plain block comments are code
375381
files:
376382
.vale.ini: |
377383
StylesPath = styles
@@ -404,15 +410,18 @@ cases:
404410
// A ZQX in a line comment is code, not documentation.
405411
406412
/*
407-
A plain block comment still has its ZQX linted.
413+
A plain block comment with a ZQX is code too.
408414
*/
415+
416+
/****************************************************
417+
** A ZQX in a license header is not documentation.
418+
****************************************************/
409419
void Widget::other() {}
410420
args: widget.cpp
411421
exit: 1
412422
want: |
413423
widget.cpp:6:23:T.Tok:'ZQX' in a doc comment
414424
widget.cpp:8:26:T.Tok:'ZQX' in a doc comment
415-
widget.cpp:15:41:T.Tok:'ZQX' in a doc comment
416425
417426
- name: qml-fragments
418427
about: doc comments in QML sources lint as QDoc through `[formats]`;
@@ -471,7 +480,7 @@ cases:
471480
//! [0]
472481
473482
/*
474-
A ZQX in a plain block comment still lints.
483+
A ZQX in a plain block comment is code as well.
475484
*/
476485
function formatValue(v) {
477486
var label = "a ZQX in a string is code";
@@ -491,7 +500,6 @@ cases:
491500
Slider.qml:10:17:T.Tok:'ZQX' in a doc comment
492501
Slider.qml:16:13:T.Note:'ZQX' in a note
493502
Slider.qml:16:13:T.Tok:'ZQX' in a doc comment
494-
Slider.qml:27:11:T.Tok:'ZQX' in a doc comment
495503
Slider.qml:36:29:T.Tok:'ZQX' in a doc comment
496504
497505
- name: quarto

‎testdata/fixtures/formats/test.qdoc‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,32 @@
4343

4444
\sa FIXME, QObject
4545
*/
46+
47+
/*! \page conditional.html
48+
\title A second title with a TODO in it
49+
50+
\qmlenum Mode
51+
\qmlsingletontype FIXME
52+
\typealias FIXME
53+
\nativetype FIXME
54+
\cmakepackage FIXME
55+
\cmakecomponent FIXME
56+
\cmaketargetitem FIXME
57+
\qtcmakepackage FIXME
58+
\qtcmaketargetitem FIXME
59+
\qtvariable FIXME
60+
\tableofcontents FIXME
61+
\dontdocument (FIXME)
62+
63+
\compareswith equality FIXME
64+
\endcompareswith
65+
66+
\if defined(FIXME)
67+
A conditional paragraph with an XXX in it.
68+
\else
69+
Another branch with a TODO in it.
70+
\endif
71+
72+
A literal backslash \\c is not a command, and \unicode 0x2022 is a
73+
character. \span {class="FIXME"} {Spanned prose with an XXX in it.}
74+
*/

0 commit comments

Comments
 (0)