Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions color.go
Original file line number Diff line number Diff line change
@@ -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")
}
26 changes: 14 additions & 12 deletions internal/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -172,16 +173,17 @@ 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, "|")
if nullable {
text += "|null"
}
param.Type = &ast.Identifier{IdentifierTkn: &token.Token{Value: []byte(text), FreeFloating: leading}}
a.added++
a.added = append(a.added, text)
return text
}

Expand Down
10 changes: 5 additions & 5 deletions internal/apply/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -281,8 +281,8 @@ func TestApply(t *testing.T) {

func TestNoTypesReturnsUnchanged(t *testing.T) {
src := "<?php\nfunction greet($who) {}"
output, count, changed := apply.Source([]byte(src), aggregate.Resolve(nil), symbols.New(), inherit.New())
if changed || count != 0 || output != src {
t.Errorf("expected unchanged, got changed=%v count=%d", changed, count)
output, added, changed := apply.Source([]byte(src), aggregate.Resolve(nil), symbols.New(), inherit.New())
if changed || len(added) != 0 || output != src {
t.Errorf("expected unchanged, got changed=%v count=%d", changed, len(added))
}
}
18 changes: 10 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"strconv"

"github.com/rectorphp/argtyper/internal/aggregate"
"github.com/rectorphp/argtyper/internal/apply"
Expand Down Expand Up @@ -99,40 +100,41 @@ func run(args []string) error {
} else {
fmt.Println("2. Adding types to parameters...")
}
added := 0
var addedTypes []string
if err := progressEach("applying", files, func(file string, src []byte) error {
output, count, changed := apply.Source(src, types, table, inheritance)
output, added, changed := apply.Source(src, types, table, inheritance)
if !changed {
return nil
}
addedTypes = append(addedTypes, added...)

if dry {
if patch, ok := diff.Lines(file, string(src), output); ok {
fmt.Print(patch)
fmt.Print(colorizePatch(patch))
}
added += count
return nil
}

if err := os.WriteFile(file, []byte(output), 0o644); err != nil {
return err
}
added += count
return nil
}); err != nil {
return err
}

if added == 0 {
if len(addedTypes) == 0 {
fmt.Println(" No new types added. Is your code that good?")
return nil
}

printOverview(addedTypes)

if dry {
fmt.Printf("\n Dry run: %d types would be added\n", added)
fmt.Printf("\n Dry run: %s types would be added\n", paint(ansiBold, strconv.Itoa(len(addedTypes))))
return nil
}

fmt.Printf(" Finished! Added %d new types\n", added)
fmt.Printf("\n %s\n", paint(ansiBold+ansiGreen, fmt.Sprintf("Finished! Added %d new types", len(addedTypes))))
return nil
}
Loading