Skip to content

chore: add support for funcorder #4810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 13, 2025
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
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ linters:
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix]
- funcorder # Checks the order of functions, methods, and constructors. [fast]
- gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false]
- gochecksumtype # Run exhaustiveness checks on Go "sum types" [fast: false, auto-fix: false]
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
Expand Down Expand Up @@ -122,6 +123,7 @@ linters:
linters:
- unused
- errcheck
- funcorder

- path: internal/tabwriter/.*_test\.go
linters:
Expand Down
14 changes: 7 additions & 7 deletions core/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ type FlagSpec struct {
EnumValues []string
}

func (node *AutoCompleteNode) addFlags(flags []FlagSpec) {
for i := range flags {
flag := &flags[i]
node.Children[flag.Name] = NewAutoCompleteFlagNode(node, flag)
}
}

// newAutoCompleteResponse builds a new AutocompleteResponse
func newAutoCompleteResponse(suggestions []string) *AutocompleteResponse {
sort.Strings(suggestions)
Expand Down Expand Up @@ -176,6 +169,13 @@ func (node *AutoCompleteNode) GetChildMatch(name string) (*AutoCompleteNode, boo
return nil, false
}

func (node *AutoCompleteNode) addFlags(flags []FlagSpec) {
for i := range flags {
flag := &flags[i]
node.Children[flag.Name] = NewAutoCompleteFlagNode(node, flag)
}
}

// isLeafCommand returns true only if n is a node with no child command (namespace, verb, resource) or a positional arg.
// A leaf command can have 2 types of children: arguments or flags
func (node *AutoCompleteNode) isLeafCommand() bool {
Expand Down
294 changes: 39 additions & 255 deletions core/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package core

import (
"context"
"fmt"
"reflect"
"sort"
"strings"

"github.com/scaleway/scaleway-cli/v2/core/human"
Expand Down Expand Up @@ -117,26 +115,6 @@ func (c *Command) Override(builder func(command *Command) *Command) {
*c = *builder(c)
}

func (c *Command) getPath() string {
if c.path != "" {
return c.path
}
path := []string(nil)
if c.Namespace != "" {
path = append(path, c.Namespace)
}
if c.Resource != "" {
path = append(path, c.Resource)
}
if c.Verb != "" {
path = append(path, c.Verb)
}

c.path = strings.Join(path, indexCommandSeparator)

return c.path
}

func (c *Command) GetCommandLine(binaryName string) string {
return strings.Trim(
binaryName+" "+strings.ReplaceAll(c.getPath(), indexCommandSeparator, " "),
Expand All @@ -162,24 +140,6 @@ func (c *Command) GetUsage(binaryName string, commands *Commands) string {
return strings.Join(parts, " ")
}

// seeAlsosAsStr returns all See Alsos as a single string
func (c *Command) seeAlsosAsStr() string {
seeAlsos := make([]string, 0, len(c.SeeAlsos))

for _, cmdSeeAlso := range c.SeeAlsos {
short := " # " + cmdSeeAlso.Short
commandStr := " " + cmdSeeAlso.Command

seeAlsoLines := []string{
short,
commandStr,
}
seeAlsos = append(seeAlsos, strings.Join(seeAlsoLines, "\n"))
}

return strings.Join(seeAlsos, "\n\n")
}

// AddInterceptors add one or multiple interceptors to a command.
// These new interceptors will be added after the already present interceptors (if any).
func (c *Command) AddInterceptors(interceptors ...CommandInterceptor) {
Expand All @@ -204,145 +164,27 @@ func (c *Command) MatchAlias(alias alias.Alias) bool {
return true
}

// Commands represent a list of CLI commands, with a index to allow searching.
type Commands struct {
commands []*Command
commandIndex map[string]*Command
}

func NewCommands(cmds ...*Command) *Commands {
c := &Commands{
commands: make([]*Command, 0, len(cmds)),
commandIndex: make(map[string]*Command, len(cmds)),
}

for _, cmd := range cmds {
c.Add(cmd)
}

return c
}

func NewCommandsMerge(cmdsList ...*Commands) *Commands {
cmdCount := 0
for _, cmds := range cmdsList {
cmdCount += len(cmds.commands)
}
c := &Commands{
commands: make([]*Command, 0, cmdCount),
commandIndex: make(map[string]*Command, cmdCount),
}
for _, cmds := range cmdsList {
for _, cmd := range cmds.commands {
c.Add(cmd)
}
}

return c
}

func (c *Commands) MustFind(path ...string) *Command {
cmd, exist := c.find(path...)
if exist {
return cmd
}

panic(fmt.Errorf("command %v not found", strings.Join(path, " ")))
}

func (c *Commands) Find(path ...string) *Command {
cmd, exist := c.find(path...)
if exist {
return cmd
}

return nil
}

func (c *Commands) Remove(namespace, verb string) {
for i := range c.commands {
if c.commands[i].Namespace == namespace && c.commands[i].Verb == verb {
c.commands = append(c.commands[:i], c.commands[i+1:]...)

return
}
}
}

func (c *Commands) RemoveResource(namespace, resource string) {
for i := range c.commands {
if c.commands[i].Namespace == namespace && c.commands[i].Resource == resource &&
c.commands[i].Verb == "" {
c.commands = append(c.commands[:i], c.commands[i+1:]...)

return
}
}
}

func (c *Commands) Add(cmd *Command) {
c.commands = append(c.commands, cmd)
c.commandIndex[cmd.getPath()] = cmd
}

func (c *Commands) Merge(cmds *Commands) {
for _, cmd := range cmds.commands {
c.Add(cmd)
}
}

func (c *Commands) MergeAll(cmds ...*Commands) {
for _, command := range cmds {
c.Merge(command)
// Copy returns a copy of a command
func (c *Command) Copy() *Command {
newCommand := *c
newCommand.Aliases = append([]string(nil), c.Aliases...)
newCommand.Examples = make([]*Example, len(c.Examples))
for i := range c.Examples {
e := *c.Examples[i]
newCommand.Examples[i] = &e
}
}

func (c *Commands) GetAll() []*Command {
return c.commands
}

// find must take the command path, eg. find("instance","get","server")
func (c *Commands) find(path ...string) (*Command, bool) {
cmd, exist := c.commandIndex[strings.Join(path, indexCommandSeparator)]
if exist {
return cmd, true
newCommand.SeeAlsos = make([]*SeeAlso, len(c.SeeAlsos))
for i := range c.SeeAlsos {
sa := *c.SeeAlsos[i]
newCommand.SeeAlsos[i] = &sa
}

return nil, false
}

// GetSortedCommand returns a slice of commands sorted alphabetically
func (c *Commands) GetSortedCommand() []*Command {
commands := make([]*Command, len(c.commands))
copy(commands, c.commands)
sort.Slice(commands, func(i, j int) bool {
return commands[i].signature() < commands[j].signature()
})

return commands
return &newCommand
}

func (c *Commands) HasSubCommands(cmd *Command) bool {
if cmd.Namespace != "" && cmd.Resource != "" && cmd.Verb != "" {
return false
}
if cmd.Namespace == "" && cmd.Resource == "" && cmd.Verb == "" {
return true
}
for _, command := range c.commands {
if command == cmd {
continue
}
if cmd.Resource == "" && cmd.Namespace == command.Namespace {
return true
}
if cmd.Verb == "" && cmd.Namespace == command.Namespace &&
cmd.Resource == command.Resource {
return true
}
}

return false
// get a signature to sort commands
func (c *Command) signature() string {
return c.Namespace + " " + c.Resource + " " + c.Verb + " " + c.Short
}

func (c *Command) getHumanMarshalerOpt() *human.MarshalOpt {
Expand All @@ -353,98 +195,40 @@ func (c *Command) getHumanMarshalerOpt() *human.MarshalOpt {
return nil
}

// get a signature to sort commands
func (c *Command) signature() string {
return c.Namespace + " " + c.Resource + " " + c.Verb + " " + c.Short
}

// AliasIsValidCommandChild returns true is alias is a valid child command of given command
// Useful for this case:
// isl => instance server list
// valid child of "instance"
// invalid child of "rdb instance"
func (c *Commands) AliasIsValidCommandChild(command *Command, alias alias.Alias) bool {
// if alias is of size one, it means it cannot be a child
if len(alias.Command) == 1 {
return true
}

// if command is verb, it cannot have children
if command.Verb != "" {
return true
}
// seeAlsosAsStr returns all See Alsos as a single string
func (c *Command) seeAlsosAsStr() string {
seeAlsos := make([]string, 0, len(c.SeeAlsos))

// if command is a resource, check command with alias' verb
if command.Resource != "" {
return c.Find(command.Namespace, command.Resource, alias.Command[1]) != nil
}
for _, cmdSeeAlso := range c.SeeAlsos {
short := " # " + cmdSeeAlso.Short
commandStr := " " + cmdSeeAlso.Command

// if command is a namespace, check for alias' verb or resource
if command.Namespace != "" {
if len(alias.Command) > 2 {
return c.Find(command.Namespace, alias.Command[1], alias.Command[2]) != nil
seeAlsoLines := []string{
short,
commandStr,
}

return c.Find(command.Namespace, alias.Command[1]) != nil
seeAlsos = append(seeAlsos, strings.Join(seeAlsoLines, "\n"))
}

return false
return strings.Join(seeAlsos, "\n\n")
}

// addAliases add valid aliases to a command
func (c *Commands) addAliases(command *Command, aliases []alias.Alias) {
names := make([]string, 0, len(aliases))
for i := range aliases {
if c.AliasIsValidCommandChild(command, aliases[i]) && command.MatchAlias(aliases[i]) {
names = append(names, aliases[i].Name)
}
func (c *Command) getPath() string {
if c.path != "" {
return c.path
}
command.Aliases = append(command.Aliases, names...)
}

// applyAliases add resource aliases to each commands
func (c *Commands) applyAliases(config *alias.Config) {
for _, command := range c.commands {
aliases := []alias.Alias(nil)
exists := false
switch {
case command.Verb != "":
aliases, exists = config.ResolveAliasesByFirstWord(command.Verb)
case command.Resource != "":
aliases, exists = config.ResolveAliasesByFirstWord(command.Resource)
case command.Namespace != "":
aliases, exists = config.ResolveAliasesByFirstWord(command.Namespace)
}
if exists {
c.addAliases(command, aliases)
}
path := []string(nil)
if c.Namespace != "" {
path = append(path, c.Namespace)
}
}

// Copy returns a copy of a command
func (c *Command) Copy() *Command {
newCommand := *c
newCommand.Aliases = append([]string(nil), c.Aliases...)
newCommand.Examples = make([]*Example, len(c.Examples))
for i := range c.Examples {
e := *c.Examples[i]
newCommand.Examples[i] = &e
if c.Resource != "" {
path = append(path, c.Resource)
}
newCommand.SeeAlsos = make([]*SeeAlso, len(c.SeeAlsos))
for i := range c.SeeAlsos {
sa := *c.SeeAlsos[i]
newCommand.SeeAlsos[i] = &sa
if c.Verb != "" {
path = append(path, c.Verb)
}

return &newCommand
}

// Copy return a copy of all commands
func (c *Commands) Copy() *Commands {
newCommands := make([]*Command, len(c.commands))
for i := range c.commands {
newCommands[i] = c.commands[i].Copy()
}
c.path = strings.Join(path, indexCommandSeparator)

return NewCommands(newCommands...)
return c.path
}
Loading
Loading