diff --git a/internal/export/export.go b/internal/export/export.go index 69a31d7f9..24fd1da87 100644 --- a/internal/export/export.go +++ b/internal/export/export.go @@ -244,18 +244,18 @@ func inGeneratedRange(line int, ranges []lint.LineRange) bool { // because such PIs sit inside a pair's body range and are skipped // here. func stripDirectives(f *lint.File, directives []directiveStrip) []byte { - stripLines := map[int]bool{} - bodyLines := map[int]bool{} + stripLines := map[int]struct{}{} + bodyLines := map[int]struct{}{} for _, d := range directives { pairs, _ := gensection.FindMarkerPairs(f, d.name, d.ruleID, d.ruleName) for _, p := range pairs { for line := p.StartLine; line < p.ContentFrom; line++ { - stripLines[line] = true + stripLines[line] = struct{}{} } - stripLines[p.EndLine] = true + stripLines[p.EndLine] = struct{}{} for line := p.ContentFrom; line <= p.ContentTo; line++ { - bodyLines[line] = true + bodyLines[line] = struct{}{} } } } @@ -276,7 +276,7 @@ func stripDirectives(f *lint.File, directives []directiveStrip) []byte { continue } for line := startLine; line <= endLine; line++ { - stripLines[line] = true + stripLines[line] = struct{}{} } } @@ -316,20 +316,20 @@ func piLineRange(pi *piparser.ProcessingInstruction, f *lint.File) (int, int) { return start, f.LineOfOffset(pi.ClosureLine.Start) } -func overlapsAny(from, to int, set map[int]bool) bool { +func overlapsAny(from, to int, set map[int]struct{}) bool { for line := from; line <= to; line++ { - if set[line] { + if _, ok := set[line]; ok { return true } } return false } -func emitLines(srcLines [][]byte, strip map[int]bool) []byte { +func emitLines(srcLines [][]byte, strip map[int]struct{}) []byte { var b bytes.Buffer for i, line := range srcLines { lineNum := i + 1 - if strip[lineNum] { + if _, ok := strip[lineNum]; ok { continue } b.Write(line) diff --git a/internal/index/locate.go b/internal/index/locate.go index ff114efed..4b62fe54e 100644 --- a/internal/index/locate.go +++ b/internal/index/locate.go @@ -462,9 +462,11 @@ func listItemValue(line string) (string, bool) { // Returns "" when none precedes the item. func enclosingListKey(lines [][]byte, line int) string { for i := line - 2; i >= 0; i-- { - m := piArgRE.FindStringSubmatch(string(lines[i])) - if len(m) >= 3 && strings.TrimSpace(m[2]) == "" { - return m[1] + // FindSubmatch accepts []byte directly, avoiding a string allocation + // per scanned line when the caller passes bytes. + m := piArgRE.FindSubmatch(lines[i]) + if len(m) >= 3 && len(m[2]) == 0 { + return string(m[1]) } // A populated `key: value` line or another list item does not // open a list block for our item; keep scanning past list items diff --git a/internal/index/locate_test.go b/internal/index/locate_test.go index f0c610f77..71355bcdb 100644 --- a/internal/index/locate_test.go +++ b/internal/index/locate_test.go @@ -248,3 +248,15 @@ func TestLocateFrontMatterKindsListItemWithDifferentValues(t *testing.T) { assert.Equal(t, "kinds", res.FrontMatterKey) assert.Equal(t, "reference", res.FrontMatterValue) } + +func TestEnclosingListKey_FindsParentKey(t *testing.T) { + lines := [][]byte{ + []byte("inputs:"), + []byte(" - alpha"), + []byte(" - beta"), + []byte(" - gamma"), + } + // Line 4 (1-based) is "gamma"; the enclosing key is "inputs". + got := enclosingListKey(lines, 4) + assert.Equal(t, "inputs", got) +} diff --git a/internal/lsp/rename.go b/internal/lsp/rename.go index 3d40fb29d..e2a232565 100644 --- a/internal/lsp/rename.go +++ b/internal/lsp/rename.go @@ -80,7 +80,8 @@ func isValidRefDefLine(source []byte, line int) bool { if bodyLine < 1 { return false } - return rename.ValidRefDefBodyLines(body)[bodyLine] + _, ok := rename.ValidRefDefBodyLines(body)[bodyLine] + return ok } // headingPrepareRange builds the rename range for an ATX or setext diff --git a/internal/rename/rename.go b/internal/rename/rename.go index 61ab96423..332d011ea 100644 --- a/internal/rename/rename.go +++ b/internal/rename/rename.go @@ -91,10 +91,10 @@ func LinkRef(source []byte, oldLabel, newName string) ([]Edit, error) { // reference definition goldmark accepted (not a code-block // look-alike). The LSP prepare-rename gate consults it so the rename // UI never surfaces on a `[label]: url`-shaped code sample. -func ValidRefDefBodyLines(body []byte) map[int]bool { - out := map[int]bool{} +func ValidRefDefBodyLines(body []byte) map[int]struct{} { + out := map[int]struct{}{} for _, m := range validRefDefMatches(body) { - out[m.bodyLine] = true + out[m.bodyLine] = struct{}{} } return out } @@ -169,7 +169,7 @@ func validRefDefMatches(body []byte) []validRefDefMatch { var out []validRefDefMatch for _, m := range index.RefDefRegexpMatches(body) { bodyLine := lineOfBodyOffset(body, m[2]) - if consumed[bodyLine] { + if _, ok := consumed[bodyLine]; ok { continue } raw := body[m[2]:m[3]] @@ -190,8 +190,8 @@ func validRefDefMatches(body []byte) []validRefDefMatch { // is by definition not a def. The Document root and // LinkReferenceDefinition nodes are skipped: the former spans the // whole buffer, the latter IS the line a real def lives on. -func contentBlockLines(root ast.Node, body []byte) map[int]bool { - out := map[int]bool{} +func contentBlockLines(root ast.Node, body []byte) map[int]struct{} { + out := map[int]struct{}{} _ = ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil @@ -206,7 +206,7 @@ func contentBlockLines(root ast.Node, body []byte) map[int]bool { ls := n.Lines() for i := 0; i < ls.Len(); i++ { seg := ls.At(i) - out[lineOfBodyOffset(body, seg.Start)] = true + out[lineOfBodyOffset(body, seg.Start)] = struct{}{} } return ast.WalkContinue, nil }) diff --git a/internal/rename/rename_test.go b/internal/rename/rename_test.go index 0f4e5b71e..5cb0103fe 100644 --- a/internal/rename/rename_test.go +++ b/internal/rename/rename_test.go @@ -88,9 +88,12 @@ func TestLinkRef_WithFrontMatterLineOffset(t *testing.T) { func TestValidRefDefBodyLines(t *testing.T) { body := []byte("para\n\n[a]: u\n\n```\n[b]: v\n```\n") + // ValidRefDefBodyLines returns map[int]struct{} — presence means valid def. got := ValidRefDefBodyLines(body) - assert.True(t, got[3], "real def on body line 3") - assert.False(t, got[6], "fenced def-shaped line is not a def") + _, ok3 := got[3] + assert.True(t, ok3, "real def on body line 3") + _, ok6 := got[6] + assert.False(t, ok6, "fenced def-shaped line is not a def") } func TestBodyAndFMOffset(t *testing.T) { diff --git a/internal/rules/concisenessscoring/rule.go b/internal/rules/concisenessscoring/rule.go index e439988d5..68e638c4f 100644 --- a/internal/rules/concisenessscoring/rule.go +++ b/internal/rules/concisenessscoring/rule.go @@ -97,17 +97,15 @@ func (r *Rule) Check(f *lint.File) []lint.Diagnostic { } line := astutil.ParagraphLine(para, f) - message := fmt.Sprintf( - "conciseness score too low (%.2f < %.2f); target >= %.2f", - result.Conciseness, r.MinScore, r.MinScore, - ) examples := formatExamples(result.Cues) + var cuesSuffix string if examples != "" { - message += fmt.Sprintf( - "; reduce verbose cues (e.g., %s)", - examples, - ) + cuesSuffix = "; reduce verbose cues (e.g., " + examples + ")" } + message := fmt.Sprintf( + "conciseness score too low (%.2f < %.2f); target >= %.2f%s", + result.Conciseness, r.MinScore, r.MinScore, cuesSuffix, + ) diags = append(diags, lint.Diagnostic{ File: f.Path, diff --git a/internal/rules/concisenessscoring/rule_test.go b/internal/rules/concisenessscoring/rule_test.go index 4f9f13545..74e728a07 100644 --- a/internal/rules/concisenessscoring/rule_test.go +++ b/internal/rules/concisenessscoring/rule_test.go @@ -296,3 +296,53 @@ func TestNewScorer_Success(t *testing.T) { require.NoError(t, err) assert.NotNil(t, s) } + +func TestCheck_MessageNoConcatenationWhenExamplesPresent(t *testing.T) { + // When verbose cues are found, the diagnostic message must include + // the cue examples in a single fmt.Sprintf rather than via `message +=` + // concatenation. This test verifies the combined message format so the + // implementation cannot silently drop the cue text. + src := []byte(verboseParagraph() + "\n") + f, err := lint.NewFile("test.md", src) + require.NoError(t, err) + + threshold := modelConciseness(t) + r := &Rule{MinScore: threshold, MinWords: 20} + diags := r.Check(f) + if len(diags) == 0 { + t.Skip("model threshold did not trigger on fixture") + } + msg := diags[0].Message + // The message must contain both the score summary and the cue guidance. + assert.Contains(t, msg, "conciseness score too low") + assert.Contains(t, msg, "target >=") + // verboseParagraph always triggers cue detection; both must be present. + assert.Contains(t, msg, "reduce verbose cues") + assert.Contains(t, msg, "e.g.,", "message with cues must include formatted examples") +} + +func TestCheck_NoCuesMessage(t *testing.T) { + // Exercises the if examples == "" branch: a paragraph that scores as + // verbose but produces no cue phrases yields the base message only. + noCuePara := "When the configuration is set, the setting is used by the " + + "configuration. The configuration uses the setting and the setting " + + "configures the configuration." + s, err := NewScorer() + require.NoError(t, err) + scored := s.Score(noCuePara) + if len(scored.Cues) > 0 { + t.Skip("scorer detected cues in fixture; model may have drifted") + } + + src := []byte(noCuePara + "\n") + f, err := lint.NewFile("test.md", src) + require.NoError(t, err) + + r := &Rule{MinScore: scored.Conciseness + 0.10, MinWords: 1} + diags := r.Check(f) + require.Len(t, diags, 1, "expected diagnostic for low-scoring no-cue paragraph") + msg := diags[0].Message + assert.Contains(t, msg, "conciseness score too low") + assert.Contains(t, msg, "target >=") + assert.NotContains(t, msg, "reduce verbose cues", "no cues detected, so no cue guidance") +} diff --git a/internal/secreview/render.go b/internal/secreview/render.go index df8b365c0..3e4e9094d 100644 --- a/internal/secreview/render.go +++ b/internal/secreview/render.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "time" ) @@ -112,9 +113,9 @@ func locStr(loc *Location) string { s = "?" } if loc.StartLine != 0 { - s += fmt.Sprintf(":%d", loc.StartLine) + s += ":" + strconv.Itoa(loc.StartLine) if loc.EndLine != 0 && loc.EndLine != loc.StartLine { - s += fmt.Sprintf("-%d", loc.EndLine) + s += "-" + strconv.Itoa(loc.EndLine) } } return s