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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ a parent or interface (including vendor), unless private or a constructor.
## Commands

```bash
argtyper add-types [project-path] [--dry] [--literals]
argtyper add-types [project-path] [--dry] [--literals] [--objects]
```

`project-path` defaults to `.`. `--dry` prints the diff without writing.
`--literals` only adds string types with a `@param 'a'|'b'` literal docblock.
`--objects` only adds object types, including unions made only of objects.
Both can be combined.

Dev tasks via `Makefile`:

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ Only add the string literal docblocks, and no other types, with `--literals`:
argtyper add-types . --literals
```

Only add object types with `--objects` - a single class, a nullable one, or a
union made only of classes (`\Foo|\Bar`). A union with a scalar, like
`int|\Money`, is skipped. Both options can be combined:

```bash
argtyper add-types . --objects
argtyper add-types . --literals --objects
```

<br>

## How it works
Expand Down
62 changes: 42 additions & 20 deletions internal/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import (
// "?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. With literalsOnly, only string
// parameters that get a `'a'|'b'` literal doc type are changed.
func Source(src []byte, types aggregate.Types, table *symbols.Table, inheritance *inherit.Table, literalsOnly bool) (string, []string, bool) {
// method would change an inherited signature. The options limit which types
// are added.
func Source(src []byte, types aggregate.Types, table *symbols.Table, inheritance *inherit.Table, options Options) (string, []string, bool) {
root, err := phpast.Parse(src)
if err != nil || root == nil {
return string(src), nil, false
}

applier := &applier{types: types, symbols: table, inheritance: inheritance, names: phpast.ResolveNames(root), literalsOnly: literalsOnly}
applier := &applier{types: types, symbols: table, inheritance: inheritance, names: phpast.ResolveNames(root), options: options}
applier.walk(root, nil)

if len(applier.added) == 0 {
Expand All @@ -37,13 +37,41 @@ func Source(src []byte, types aggregate.Types, table *symbols.Table, inheritance
return phpast.Print(root), applier.added, true
}

// Options limit the added types to some kinds. With none set, every type is
// added; with one or more set, only the chosen kinds are.
type Options struct {
Literals bool // string types that get a `'a'|'b'` literal doc type
Objects bool // object types, including unions made only of objects
}

// allows reports whether a resolved type passes the options; docType is its
// literal doc type, if any.
func (o Options) allows(resolved aggregate.Resolved, docType string) bool {
if !o.Literals && !o.Objects {
return true
}
if o.Literals && docType != "" {
return true
}
return o.Objects && onlyObjects(resolved)
}

func onlyObjects(resolved aggregate.Resolved) bool {
for _, member := range resolved.Types {
if !strings.HasPrefix(member, "object:") {
return false
}
}
return len(resolved.Types) > 0
}

type applier struct {
types aggregate.Types
symbols *symbols.Table
inheritance *inherit.Table
names map[ast.Vertex]string
added []string
literalsOnly bool
types aggregate.Types
symbols *symbols.Table
inheritance *inherit.Table
names map[ast.Vertex]string
added []string
options Options
}

func (a *applier) walk(node ast.Vertex, class *ast.StmtClass) {
Expand Down Expand Up @@ -77,7 +105,7 @@ func (a *applier) applyFunction(function *ast.StmtFunction) {
}
if resolved, ok := a.types.Function(name, position); ok {
docType := literalDocType(param, resolved)
if a.literalsOnly && docType == "" {
if !a.options.allows(resolved, docType) {
continue
}
added[phpast.VariableName(param.Var)] = a.setType(param, resolved)
Expand All @@ -87,10 +115,7 @@ func (a *applier) applyFunction(function *ast.StmtFunction) {
}
continue
}
if a.literalsOnly {
continue
}
if resolved, ok := a.defaultType(param, "", ""); ok {
if resolved, ok := a.defaultType(param, "", ""); ok && a.options.allows(resolved, "") {
added[phpast.VariableName(param.Var)] = a.setType(param, resolved)
}
}
Expand Down Expand Up @@ -124,7 +149,7 @@ func (a *applier) applyMethod(method *ast.StmtClassMethod, class *ast.StmtClass)
}
if resolved, ok := a.types.Method(className, name, position); ok {
docType := literalDocType(param, resolved)
if a.literalsOnly && docType == "" {
if !a.options.allows(resolved, docType) {
continue
}
added[phpast.VariableName(param.Var)] = a.setType(param, resolved)
Expand All @@ -134,10 +159,7 @@ func (a *applier) applyMethod(method *ast.StmtClassMethod, class *ast.StmtClass)
}
continue
}
if a.literalsOnly {
continue
}
if resolved, ok := a.defaultType(param, className, classFQCN); ok {
if resolved, ok := a.defaultType(param, className, classFQCN); ok && a.options.allows(resolved, "") {
added[phpast.VariableName(param.Var)] = a.setType(param, resolved)
}
}
Expand Down
33 changes: 30 additions & 3 deletions internal/apply/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func run(target string, sources ...string) (string, int) {
records = append(records, collect.FromSource([]byte(source), table)...)
}
types := aggregate.Resolve(records)
output, added, _ := apply.Source([]byte(target), types, table, inheritance, false)
output, added, _ := apply.Source([]byte(target), types, table, inheritance, apply.Options{})
return output, len(added)
}

Expand Down Expand Up @@ -354,7 +354,7 @@ func TestApply(t *testing.T) {

func TestNoTypesReturnsUnchanged(t *testing.T) {
src := "<?php\nfunction greet($who) {}"
output, added, changed := apply.Source([]byte(src), aggregate.Resolve(nil), symbols.New(), inherit.New(), false)
output, added, changed := apply.Source([]byte(src), aggregate.Resolve(nil), symbols.New(), inherit.New(), apply.Options{})
if changed || len(added) != 0 || output != src {
t.Errorf("expected unchanged, got changed=%v count=%d", changed, len(added))
}
Expand All @@ -365,10 +365,37 @@ func TestLiteralsOnly(t *testing.T) {
table := symbols.New()
types := aggregate.Resolve(collect.FromSource([]byte(src), table))

output, added, _ := apply.Source([]byte(src), types, table, inherit.New(), true)
output, added, _ := apply.Source([]byte(src), types, table, inherit.New(), apply.Options{Literals: true})

want := "<?php\n/**\n * @param 'a'|'b' $v\n */\nfunction pick(string $v, $count, $page = 1) {}\npick('a', 1);\npick('b', 2);"
if output != want || len(added) != 1 {
t.Errorf("\n got: %q (%d added)\nwant: %q", output, len(added), want)
}
}

func TestObjectsOnly(t *testing.T) {
src := "<?php\nfunction save($user, $item, $count, $mixed, $status = Status::Active) {}\nenum Status {\n case Active;\n}\nsave(new User(), new Item(), 1, new Money());\nsave(null, new Order(), 2, 5);"
table := symbols.New()
table.CollectSource([]byte(src))
types := aggregate.Resolve(collect.FromSource([]byte(src), table))

output, added, _ := apply.Source([]byte(src), types, table, inherit.New(), apply.Options{Objects: true})

want := "<?php\nfunction save(?\\User $user, \\Item|\\Order $item, $count, $mixed, \\Status $status = Status::Active) {}\nenum Status {\n case Active;\n}\nsave(new User(), new Item(), 1, new Money());\nsave(null, new Order(), 2, 5);"
if output != want || len(added) != 3 {
t.Errorf("\n got: %q (%d added)\nwant: %q", output, len(added), want)
}
}

func TestLiteralsAndObjects(t *testing.T) {
src := "<?php\nfunction save($user, $mode, $count) {}\nsave(new User(), 'a', 1);\nsave(new User(), 'b', 2);"
table := symbols.New()
types := aggregate.Resolve(collect.FromSource([]byte(src), table))

output, added, _ := apply.Source([]byte(src), types, table, inherit.New(), apply.Options{Literals: true, Objects: true})

want := "<?php\n/**\n * @param 'a'|'b' $mode\n */\nfunction save(\\User $user, string $mode, $count) {}\nsave(new User(), 'a', 1);\nsave(new User(), 'b', 2);"
if output != want || len(added) != 2 {
t.Errorf("\n got: %q (%d added)\nwant: %q", output, len(added), want)
}
}
17 changes: 12 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import (
"github.com/rectorphp/argtyper/internal/symbols"
)

const usage = `Usage: argtyper add-types [project-path] [--dry] [--literals]
const usage = `Usage: argtyper add-types [project-path] [--dry] [--literals] [--objects]

Find literal values passed into local method/function calls and add them as
parameter type declarations. Defaults to the current directory.

--dry Print the diff of the types that would be added, without writing.
--literals Only add string types with a @param 'a'|'b' literal docblock.`
--literals Only add string types with a @param 'a'|'b' literal docblock.
--objects Only add object types, including unions made only of objects.

--literals and --objects can be combined.`

func main() {
if err := run(os.Args[1:]); err != nil {
Expand All @@ -41,15 +44,19 @@ func run(args []string) error {
}

dry := false
literalsOnly := false
options := apply.Options{}
projectPath := "."
for _, arg := range args[1:] {
if arg == "--dry" {
dry = true
continue
}
if arg == "--literals" {
literalsOnly = true
options.Literals = true
continue
}
if arg == "--objects" {
options.Objects = true
continue
}
projectPath = arg
Expand Down Expand Up @@ -114,7 +121,7 @@ func run(args []string) error {
}
var addedTypes []string
if err := progressEach("applying", files, func(file string, src []byte) error {
output, added, changed := apply.Source(src, types, table, inheritance, literalsOnly)
output, added, changed := apply.Source(src, types, table, inheritance, options)
if !changed {
return nil
}
Expand Down
Loading