diff --git a/color.go b/color.go new file mode 100644 index 00000000..d46ae113 --- /dev/null +++ b/color.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +// ANSI SGR codes for the summary and diff output. Emitted only when +// colorEnabled is true, so a redirected or piped run stays plain text. +const ( + ansiReset = "\x1b[0m" + ansiBold = "\x1b[1m" + ansiDim = "\x1b[2m" + ansiRed = "\x1b[31m" + ansiGreen = "\x1b[32m" + ansiYellow = "\x1b[33m" + ansiMagenta = "\x1b[35m" + ansiCyan = "\x1b[36m" +) + +// stdoutIsTerminal reports whether stdout is an interactive terminal. The diff +// and summary print to stdout, so color keys off it; a redirected run gets +// plain text and the NO_COLOR convention also disables it. +var stdoutIsTerminal = detectStdout() + +var colorEnabled = stdoutIsTerminal && os.Getenv("NO_COLOR") == "" + +func detectStdout() bool { + fileInfo, err := os.Stdout.Stat() + if err != nil { + return false + } + return fileInfo.Mode()&os.ModeCharDevice != 0 +} + +// paint wraps text in an ANSI code when color is enabled, untouched otherwise. +func paint(code string, text string) string { + if !colorEnabled { + return text + } + return code + text + ansiReset +} + +// classifyType buckets a written type declaration ("int", "?string", "\Foo", +// "int|null") into a category for the summary. +func classifyType(text string) string { + base := strings.TrimPrefix(text, "?") + base = strings.TrimSuffix(base, "|null") + if strings.Contains(base, "|") { + return "union" + } + switch base { + case "int", "float", "string", "bool": + return "scalar" + case "array", "iterable": + return "array" + } + if strings.HasPrefix(base, "\\") { + return "object" + } + return "other" +} + +// category rows are printed in this fixed order, each with its own color. +var categoryOrder = []string{"scalar", "object", "array", "union", "other"} + +var categoryLabel = map[string]string{ + "scalar": "scalar types", + "object": "object types", + "array": "array types", + "union": "union types", + "other": "other types", +} + +var categoryColor = map[string]string{ + "scalar": ansiCyan, + "object": ansiGreen, + "array": ansiYellow, + "union": ansiMagenta, + "other": ansiDim, +} + +// printOverview prints a colored breakdown of the added types by category. +func printOverview(addedTypes []string) { + counts := map[string]int{} + for _, text := range addedTypes { + counts[classifyType(text)]++ + } + + fmt.Println("\n Added types by category:") + for _, key := range categoryOrder { + count := counts[key] + if count == 0 { + continue + } + label := fmt.Sprintf("%-13s", categoryLabel[key]) + fmt.Printf(" %s %s\n", paint(categoryColor[key], label), paint(ansiBold, strconv.Itoa(count))) + } +} + +// colorizePatch colors a dry-run diff: added lines green, removed lines red. +func colorizePatch(patch string) string { + if !colorEnabled { + return patch + } + lines := strings.Split(patch, "\n") + for index, line := range lines { + switch { + case strings.HasPrefix(line, " + "): + lines[index] = paint(ansiGreen, line) + case strings.HasPrefix(line, " - "): + lines[index] = paint(ansiRed, line) + } + } + return strings.Join(lines, "\n") +} diff --git a/internal/apply/apply.go b/internal/apply/apply.go index f60d6a98..1bf7615e 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -14,21 +14,22 @@ import ( ) // Source adds parameter types to a single PHP source file. It returns the new -// source, the number of types added, and whether the file changed. On a parse -// error the original source is returned unchanged. The symbols table resolves -// constant and enum-case default values; the inheritance table decides whether -// typing a method would change an inherited signature. -func Source(src []byte, types aggregate.Types, table *symbols.Table, inheritance *inherit.Table) (string, int, bool) { +// source, the type declarations added (their written text, e.g. "int" or +// "?string" or "\Foo"), and whether the file changed. On a parse error the +// original source is returned unchanged. The symbols table resolves constant +// and enum-case default values; the inheritance table decides whether typing a +// method would change an inherited signature. +func Source(src []byte, types aggregate.Types, table *symbols.Table, inheritance *inherit.Table) (string, []string, bool) { root, err := phpast.Parse(src) if err != nil || root == nil { - return string(src), 0, false + return string(src), nil, false } applier := &applier{types: types, symbols: table, inheritance: inheritance, names: phpast.ResolveNames(root)} applier.walk(root, nil) - if applier.added == 0 { - return string(src), 0, false + if len(applier.added) == 0 { + return string(src), nil, false } return phpast.Print(root), applier.added, true @@ -39,7 +40,7 @@ type applier struct { symbols *symbols.Table inheritance *inherit.Table names map[ast.Vertex]string - added int + added []string } func (a *applier) walk(node ast.Vertex, class *ast.StmtClass) { @@ -172,8 +173,9 @@ func (a *applier) setType(param *ast.Parameter, resolved aggregate.Resolved) str QuestionTkn: &token.Token{Value: []byte("?"), FreeFloating: leading}, Expr: &ast.Identifier{IdentifierTkn: &token.Token{Value: []byte(members[0])}}, } - a.added++ - return "?" + members[0] + nullableText := "?" + members[0] + a.added = append(a.added, nullableText) + return nullableText } text := strings.Join(members, "|") @@ -181,7 +183,7 @@ func (a *applier) setType(param *ast.Parameter, resolved aggregate.Resolved) str text += "|null" } param.Type = &ast.Identifier{IdentifierTkn: &token.Token{Value: []byte(text), FreeFloating: leading}} - a.added++ + a.added = append(a.added, text) return text } diff --git a/internal/apply/apply_test.go b/internal/apply/apply_test.go index 0283c085..a303faee 100644 --- a/internal/apply/apply_test.go +++ b/internal/apply/apply_test.go @@ -23,8 +23,8 @@ func run(target string, sources ...string) (string, int) { records = append(records, collect.FromSource([]byte(source), table)...) } types := aggregate.Resolve(records) - output, count, _ := apply.Source([]byte(target), types, table, inheritance) - return output, count + output, added, _ := apply.Source([]byte(target), types, table, inheritance) + return output, len(added) } func TestApply(t *testing.T) { @@ -281,8 +281,8 @@ func TestApply(t *testing.T) { func TestNoTypesReturnsUnchanged(t *testing.T) { src := "