feat(processor): StripComments chroma engine with legacy fallback

This commit is contained in:
2026-07-02 09:20:06 -05:00
parent 82b84c12d2
commit 811f1c13ff
2 changed files with 199 additions and 0 deletions
+159
View File
@@ -6,6 +6,8 @@ import (
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/lexers"
"github.com/carlosarraes/shush/internal/config"
"github.com/carlosarraes/shush/internal/types"
) )
type Options struct { type Options struct {
@@ -61,3 +63,160 @@ func leadingWhitespace(s string) string {
} }
return s return s
} }
func StripComments(src []byte, filename string, cfg *config.Config, opts Options) ([]byte, []LineChange, error) {
lexer := resolveLexer(filename)
if lexer == nil {
return legacyStrip(src, filename, cfg, opts)
}
it, err := lexer.Tokenise(nil, string(src))
if err != nil {
return legacyStrip(src, filename, cfg, opts)
}
toks := it.Tokens()
var check strings.Builder
for _, t := range toks {
check.WriteString(t.Value)
}
if check.String() != string(src) {
return legacyStrip(src, filename, cfg, opts)
}
origLines := strings.Split(string(src), "\n")
origLineText := func(n int) string {
if n >= 1 && n-1 < len(origLines) {
return origLines[n-1]
}
return ""
}
var changes []LineChange
var out strings.Builder
var cur strings.Builder
line := 1
hadDrop := false
hadPreserved := false
flush := func() {
result := cur.String()
orig := origLineText(line)
action := "kept"
emit := true
if hadDrop {
trimmed := strings.TrimRight(result, " \t")
if strings.TrimSpace(trimmed) == "" {
action = "removed"
if opts.PreserveLines {
result = leadingWhitespace(orig)
} else {
emit = false
result = ""
}
} else {
result = trimmed
action = "modified"
}
} else if hadPreserved {
action = "preserved"
}
changes = append(changes, LineChange{Num: line, Original: orig, Result: result, Action: action})
if emit {
out.WriteString(result)
out.WriteString("\n")
}
cur.Reset()
hadDrop = false
hadPreserved = false
line++
}
for _, t := range toks {
startLine := line
endLine := startLine + strings.Count(t.Value, "\n")
drop := shouldDrop(t.Type, opts)
if drop && cfg != nil && cfg.ShouldPreserveComment(t.Value) {
drop = false
}
if drop && opts.InRange != nil {
for ln := startLine; ln <= endLine; ln++ {
if !opts.InRange(ln) {
drop = false
break
}
}
}
segs := strings.Split(t.Value, "\n")
for si, seg := range segs {
if drop {
if seg != "" {
hadDrop = true
}
} else {
cur.WriteString(seg)
if seg != "" && isCommentType(t.Type) {
hadPreserved = true
}
}
if si < len(segs)-1 {
flush()
}
}
}
if cur.Len() > 0 {
flush()
}
return []byte(out.String()), changes, nil
}
func legacyStrip(src []byte, filename string, cfg *config.Config, opts Options) ([]byte, []LineChange, error) {
lang, err := DetectLanguage(filename)
if err != nil {
return src, nil, err
}
p := &Processor{cli: types.CLI{Inline: opts.Inline, Block: opts.Block, PreserveLines: opts.PreserveLines}}
text := strings.TrimSuffix(string(src), "\n")
var lines []string
if len(src) > 0 {
lines = strings.Split(text, "\n")
}
var changes []LineChange
var out strings.Builder
for i, ln := range lines {
num := i + 1
process := opts.InRange == nil || opts.InRange(num)
result := ln
action := "kept"
emit := true
if process {
nl := p.removeCommentsFromLine(ln, lang, cfg)
if nl != ln {
if nl == "" {
action = "removed"
emit = false
} else {
action = "modified"
result = nl
}
}
}
changes = append(changes, LineChange{Num: num, Original: ln, Result: result, Action: action})
if emit {
out.WriteString(result)
out.WriteString("\n")
}
}
return []byte(out.String()), changes, nil
}
func changed(changes []LineChange) bool {
for _, c := range changes {
if c.Action == "modified" || c.Action == "removed" {
return true
}
}
return false
}
+40
View File
@@ -1,9 +1,11 @@
package processor package processor
import ( import (
"bytes"
"testing" "testing"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/carlosarraes/shush/internal/config"
) )
func TestResolveLexer(t *testing.T) { func TestResolveLexer(t *testing.T) {
@@ -56,3 +58,41 @@ func TestShouldDrop(t *testing.T) {
} }
} }
} }
func TestStripComments(t *testing.T) {
def := config.Default()
todo := &config.Config{Preserve: []string{"TODO:"}, ContextLines: 3}
cases := []struct {
name string
file string
src string
cfg *config.Config
opts Options
want string
}{
{"multiline block collapses", "a.c", "code();\n/* a\n b */\nmore();\n", def, Options{}, "code();\nmore();\n"},
{"inline block both sides kept", "a.c", "x(); /* c */ y();\n", def, Options{}, "x(); y();\n"},
{"preproc kept comment dropped", "a.c", "#include <stdio.h>\nint x; // c\n", def, Options{}, "#include <stdio.h>\nint x;\n"},
{"unterminated block to eof", "a.c", "x();\n/* no end\nmore\n", def, Options{}, "x();\n"},
{"marker inside line comment inert", "a.c", "z(); // a /* b\nw();\n", def, Options{}, "z();\nw();\n"},
{"raw string preserved", "a.go", "s := `a /* b */ c`\n", def, Options{}, "s := `a /* b */ c`\n"},
{"string marker preserved", "a.js", "console.log(\"/* x */\"); /* r */ y();\n", def, Options{}, "console.log(\"/* x */\"); y();\n"},
{"preserve pattern keeps line", "a.go", "x := 1 // TODO: keep\n", todo, Options{}, "x := 1 // TODO: keep\n"},
{"preserve-lines blanks comment-only", "a.py", " # c\nx = 1\n", def, Options{PreserveLines: true}, " \nx = 1\n"},
{"inline flag keeps block", "a.c", "code(); /* b */ // l\n", def, Options{Inline: true}, "code(); /* b */\n"},
{"block flag keeps line", "a.c", "code(); /* b */ // l\n", def, Options{Block: true}, "code(); // l\n"},
{"straddle block kept", "a.c", "/* a\n b */ x()\n", def, Options{InRange: func(n int) bool { return n == 2 }}, "/* a\n b */ x()\n"},
{"range block removed when fully inside", "a.c", "x()\n/* a\n b */\ny()\n", def, Options{InRange: func(n int) bool { return n >= 2 && n <= 3 }}, "x()\ny()\n"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out, _, err := StripComments([]byte(c.src), c.file, c.cfg, c.opts)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(out, []byte(c.want)) {
t.Errorf("got %q want %q", out, c.want)
}
})
}
}