64314aee
feat(scanner): support multi-language comment syntax
a73x 2026-04-29 05:44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index dc41796..5da726c 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -15,9 +15,31 @@ type Marker struct { UUID string } // commentPrefixes maps file extension (lowercase, with dot) to the line-comment prefix. var commentPrefixes = map[string]string{ ".go": "//", // lineCommentPrefixes: extensions whose markers must be a line-comment. var lineCommentPrefixes = map[string]string{ ".go": "//", ".js": "//", ".jsx": "//", ".ts": "//", ".tsx": "//", ".c": "//", ".cpp": "//", ".h": "//", ".hpp": "//", ".rs": "//", ".java": "//", ".kt": "//", ".swift": "//", ".scala": "//", ".cs": "//", ".py": "#", ".rb": "#", ".sh": "#", ".bash": "#", ".zsh": "#", ".yaml": "#", ".yml": "#", ".toml": "#", ".sql": "--", ".lua": "--", ".hs": "--", ".elm": "--", } // blockCommentDelims: extensions whose single-line markers may use a block comment. type blockDelim struct{ open, close string } var blockCommentDelims = map[string]blockDelim{ ".css": {"/*", "*/"}, ".html": {"<!--", "-->"}, ".xml": {"<!--", "-->"}, ".md": {"<!--", "-->"}, } // nameOverrides: filename → line-comment prefix when no useful extension exists. var nameOverrides = map[string]string{ "Makefile": "#", "makefile": "#", "Dockerfile": "#", "dockerfile": "#", } var ( @@ -27,10 +49,15 @@ var ( func Scan(path string) ([]Marker, error) { ext := strings.ToLower(filepath.Ext(path)) prefix, ok := commentPrefixes[ext] if !ok { linePrefix, hasLine := lineCommentPrefixes[ext] if !hasLine { linePrefix, hasLine = nameOverrides[filepath.Base(path)] } blockD, hasBlock := blockCommentDelims[ext] if !hasLine && !hasBlock { return nil, nil } f, err := os.Open(path) if err != nil { return nil, err @@ -44,10 +71,15 @@ func Scan(path string) ([]Marker, error) { for sc.Scan() { line++ raw := strings.TrimSpace(sc.Text()) if !strings.HasPrefix(raw, prefix) { var body string switch { case hasLine && strings.HasPrefix(raw, linePrefix): body = strings.TrimSpace(strings.TrimPrefix(raw, linePrefix)) case hasBlock && strings.HasPrefix(raw, blockD.open) && strings.HasSuffix(raw, blockD.close): body = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(raw, blockD.open), blockD.close)) default: continue } body := strings.TrimSpace(strings.TrimPrefix(raw, prefix)) if m := taggedRE.FindStringSubmatch(body); m != nil { markers = append(markers, Marker{Line: line, Text: body, Tagged: true, UUID: m[1]}) continue diff --git a/internal/scanner/scanner_test.go b/internal/scanner/scanner_test.go index a097963..1b08be2 100644 --- a/internal/scanner/scanner_test.go +++ b/internal/scanner/scanner_test.go @@ -37,3 +37,75 @@ func TestScanGoFileSingleUntaggedMarker(t *testing.T) { t.Errorf("Tagged = true, want false") } } func TestScanPythonFile(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "foo.py", "# @claude rename this\ndef foo(): pass\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 1 || markers[0].Line != 1 || markers[0].Text != "@claude rename this" { t.Fatalf("got %#v", markers) } } func TestScanSQLFile(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "q.sql", "-- @claude add an index\nSELECT 1;\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 1 || markers[0].Text != "@claude add an index" { t.Fatalf("got %#v", markers) } } func TestScanCSSBlockComment(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "s.css", "/* @claude make this responsive */\nbody {}\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 1 || markers[0].Text != "@claude make this responsive" { t.Fatalf("got %#v", markers) } } func TestScanHTMLBlockComment(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "x.html", "<!-- @claude add aria labels -->\n<div></div>\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 1 || markers[0].Text != "@claude add aria labels" { t.Fatalf("got %#v", markers) } } func TestScanUnrecognizedExtensionSkipped(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "weird.xyz", "// @claude do thing\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 0 { t.Fatalf("expected skip, got %#v", markers) } } func TestScanMakefileByName(t *testing.T) { dir := t.TempDir() path := writeFile(t, dir, "Makefile", "# @claude add a lint target\nbuild:\n\techo hi\n") markers, err := Scan(path) if err != nil { t.Fatal(err) } if len(markers) != 1 || markers[0].Text != "@claude add a lint target" { t.Fatalf("got %#v", markers) } }