a73x

internal/scanner/scanner.go

Ref:   Size: 2.9 KiB

package scanner

import (
	"bufio"
	"bytes"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

type Marker struct {
	Line   int
	Text   string
	Tagged bool
	UUID   string
}

// 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 (
	untaggedRE = regexp.MustCompile(`^@claude\s+\S`)
	taggedRE   = regexp.MustCompile(`^@claude\[(fw-[0-9a-f]{8})\]\s+\S`)
)

func Scan(path string) ([]Marker, error) {
	ext := strings.ToLower(filepath.Ext(path))
	linePrefix, hasLine := lineCommentPrefixes[ext]
	if !hasLine {
		linePrefix, hasLine = nameOverrides[filepath.Base(path)]
	}
	blockD, hasBlock := blockCommentDelims[ext]
	if !hasLine && !hasBlock {
		return nil, nil
	}

	const maxBytes = 1 << 20 // 1 MB

	st, err := os.Stat(path)
	if err != nil {
		return nil, err
	}
	if st.Size() > maxBytes {
		return nil, nil
	}

	// Binary sniff: read first 8 KB, look for null byte.
	sniffN := min(int64(8192), st.Size())
	sniff := make([]byte, sniffN)
	sf, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	n, _ := sf.Read(sniff)
	sf.Close()
	if bytes.IndexByte(sniff[:n], 0) >= 0 {
		return nil, nil
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var markers []Marker
	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 1024*1024), 1024*1024)
	line := 0
	for sc.Scan() {
		line++
		raw := strings.TrimSpace(sc.Text())
		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
		}
		if m := taggedRE.FindStringSubmatch(body); m != nil {
			markers = append(markers, Marker{Line: line, Text: body, Tagged: true, UUID: m[1]})
			continue
		}
		if untaggedRE.MatchString(body) {
			markers = append(markers, Marker{Line: line, Text: body})
		}
	}
	return markers, sc.Err()
}