Files
2026-06-17 16:29:33 +08:00

351 lines
9.0 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package rules
import (
"errors"
"fmt"
"strings"
"unicode"
"github.com/larksuite/cli/internal/qualitygate/facts"
"github.com/larksuite/cli/internal/qualitygate/manifest"
"github.com/larksuite/cli/internal/qualitygate/report"
"github.com/larksuite/cli/internal/qualitygate/skillscan"
)
var errUnknownCommand = errors.New("unknown command")
type ParsedExample struct {
CommandPath string
Flags []string
Positional []string
}
type ReferencePolicy struct {
Incremental bool
ChangedFiles map[string]bool
CommandSurfaceAffected bool
BaseManifest *manifest.Manifest
BaseManifestComplete bool
}
func CheckReferences(m manifest.Manifest, examples []skillscan.Example) ([]report.Diagnostic, []facts.SkillFact) {
return CheckReferencesWithPolicy(m, examples, ReferencePolicy{})
}
func CheckReferencesWithPolicy(m manifest.Manifest, examples []skillscan.Example, policy ReferencePolicy) ([]report.Diagnostic, []facts.SkillFact) {
index := indexManifest(m)
var diags []report.Diagnostic
var skillFacts []facts.SkillFact
for _, ex := range examples {
fact := facts.SkillFact{SourceFile: ex.SourceFile, Line: ex.Line, Raw: ex.Raw}
parsed, err := parseAgainstManifest(m, ex.Raw)
if err != nil {
if errors.Is(err, errUnknownCommand) && commandPathContainsPlaceholder(ex.Raw) {
skillFacts = append(skillFacts, fact)
continue
}
if errors.Is(err, errUnknownCommand) {
fact.ReferencesInvalidCommand = true
diags = append(diags, applyReferencePolicy(rejectUnknownCommand(ex, unknownCommandPath(ex.Raw)), ex, policy))
} else {
diags = append(diags, parseWarning(ex, err))
}
skillFacts = append(skillFacts, fact)
continue
}
fact.CommandPath = parsed.CommandPath
for _, flag := range parsed.Flags {
if index.hasFlag(parsed.CommandPath, flag) {
continue
}
fact.ReferencesInvalidCommand = true
diags = append(diags, applyReferencePolicy(rejectUnknownFlag(ex, parsed.CommandPath, flag), ex, policy))
}
skillFacts = append(skillFacts, fact)
}
return diags, skillFacts
}
func applyReferencePolicy(diag report.Diagnostic, ex skillscan.Example, policy ReferencePolicy) report.Diagnostic {
if diag.Action != report.ActionReject || !policy.Incremental {
return diag
}
sourceFile := normalizeReferencePath(ex.SourceFile)
if !strings.HasPrefix(sourceFile, "skills/") || policy.ChangedFiles[sourceFile] {
return diag
}
if referenceBecameInvalid(ex, policy) {
return diag
}
diag.Action = report.ActionWarning
diag.Suggestion = "unchanged legacy skill reference; fix in a skill-specific PR or update the changed skill file before this becomes blocking"
return diag
}
func referenceBecameInvalid(ex skillscan.Example, policy ReferencePolicy) bool {
if !policy.CommandSurfaceAffected {
return false
}
if policy.BaseManifest == nil {
return false
}
if _, err := parseAgainstManifest(*policy.BaseManifest, ex.Raw); err == nil {
return true
}
return false
}
func normalizeReferencePath(value string) string {
return strings.TrimPrefix(strings.ReplaceAll(value, "\\", "/"), "./")
}
func parseAgainstManifest(m manifest.Manifest, raw string) (ParsedExample, error) {
argv, err := splitShellWords(raw)
if err != nil {
return ParsedExample{}, err
}
if len(argv) == 0 || argv[0] != "lark-cli" {
return ParsedExample{}, fmt.Errorf("not a lark-cli command")
}
idx := indexManifest(m)
for end := len(argv); end > 1; end-- {
candidate := strings.Join(argv[1:end], " ")
cmd := idx.commands[candidate]
if cmd == nil {
continue
}
remaining := argv[end:]
if idx.children[candidate] && startsWithCommandToken(remaining) {
continue
}
flags, positional, err := consumeFlags(remaining, cmd)
if err != nil {
return ParsedExample{}, err
}
return ParsedExample{CommandPath: candidate, Flags: flags, Positional: positional}, nil
}
return ParsedExample{}, errUnknownCommand
}
func commandPathContainsPlaceholder(raw string) bool {
argv, err := splitShellWords(raw)
if err != nil {
return false
}
for _, arg := range argv[1:] {
if isShellOperator(arg) || strings.HasPrefix(arg, "-") {
return false
}
if skillscan.HasPlaceholder(arg) {
return true
}
}
return false
}
func startsWithCommandToken(args []string) bool {
return len(args) > 0 && args[0] != "--" && !strings.HasPrefix(args[0], "-")
}
func consumeFlags(args []string, cmd *manifest.Command) ([]string, []string, error) {
var flags []string
var positional []string
for i := 0; i < len(args); i++ {
arg := args[i]
if isShellOperator(arg) {
break
}
if arg == "--" {
positional = append(positional, args[i+1:]...)
break
}
if strings.HasPrefix(arg, "--") {
name := strings.TrimPrefix(arg, "--")
hasInlineValue := false
if eq := strings.IndexByte(name, '='); eq >= 0 {
name = name[:eq]
hasInlineValue = true
}
flag := findManifestFlag(cmd, name)
flags = append(flags, name)
if flag != nil && !hasInlineValue && flag.TakesValue && i+1 < len(args) {
i++
}
continue
}
if strings.HasPrefix(arg, "-") && len(arg) > 1 {
name := strings.TrimPrefix(arg, "-")
if name == "h" {
name = "help"
}
if flag := findManifestFlag(cmd, name); flag != nil {
name = flag.Name
if flag.TakesValue && i+1 < len(args) {
i++
}
}
flags = append(flags, name)
continue
}
positional = append(positional, arg)
}
return flags, positional, nil
}
func isShellOperator(arg string) bool {
return arg == "#" || arg == "|" || arg == "&&" || arg == "||" || arg == ">" || arg == "2>" || arg == "<"
}
func findManifestFlag(cmd *manifest.Command, name string) *manifest.Flag {
for i := range cmd.Flags {
if cmd.Flags[i].Name == name || cmd.Flags[i].Shorthand == name {
return &cmd.Flags[i]
}
}
return nil
}
func unknownCommandPath(raw string) string {
argv, err := splitShellWords(raw)
if err != nil || len(argv) <= 1 {
return ""
}
var parts []string
for _, arg := range argv[1:] {
if strings.HasPrefix(arg, "-") {
break
}
parts = append(parts, arg)
}
return strings.Join(parts, " ")
}
type manifestIndex struct {
commands map[string]*manifest.Command
children map[string]bool
flags map[string]map[string]bool
}
func indexManifest(m manifest.Manifest) manifestIndex {
index := manifestIndex{
commands: make(map[string]*manifest.Command, len(m.Commands)),
children: make(map[string]bool, len(m.Commands)),
flags: make(map[string]map[string]bool, len(m.Commands)),
}
for i := range m.Commands {
cmd := &m.Commands[i]
index.commands[cmd.Path] = cmd
flagSet := make(map[string]bool, len(cmd.Flags))
for _, fl := range cmd.Flags {
flagSet[fl.Name] = true
}
index.flags[cmd.Path] = flagSet
}
for _, cmd := range m.Commands {
parts := strings.Fields(cmd.Path)
for n := 1; n < len(parts); n++ {
parent := strings.Join(parts[:n], " ")
if index.commands[parent] != nil {
index.children[parent] = true
}
}
}
return index
}
func (i manifestIndex) hasFlag(commandPath, flag string) bool {
if flag == "help" {
return true
}
return i.flags[commandPath][flag]
}
func splitShellWords(raw string) ([]string, error) {
var words []string
var b strings.Builder
var quote rune
escaped := false
inWord := false
for _, r := range raw {
if escaped {
b.WriteRune(r)
escaped = false
inWord = true
continue
}
if quote != '\'' && r == '\\' {
escaped = true
inWord = true
continue
}
if quote != 0 {
if r == quote {
quote = 0
continue
}
b.WriteRune(r)
inWord = true
continue
}
switch {
case r == '\'' || r == '"':
quote = r
inWord = true
case unicode.IsSpace(r):
if inWord {
words = append(words, b.String())
b.Reset()
inWord = false
}
default:
b.WriteRune(r)
inWord = true
}
}
if escaped {
b.WriteRune('\\')
}
if quote != 0 {
return nil, fmt.Errorf("unterminated quote")
}
if inWord {
words = append(words, b.String())
}
return words, nil
}
func parseWarning(ex skillscan.Example, err error) report.Diagnostic {
return report.Diagnostic{
Rule: "skill_command_parse",
Action: report.ActionWarning,
File: ex.SourceFile,
Line: ex.Line,
Message: fmt.Sprintf("cannot parse lark-cli example: %v", err),
}
}
func rejectUnknownCommand(ex skillscan.Example, commandPath string) report.Diagnostic {
return report.Diagnostic{
Rule: "skill_command_reference",
Action: report.ActionReject,
File: ex.SourceFile,
Line: ex.Line,
Message: fmt.Sprintf("example references unknown command %q", commandPath),
Suggestion: "update the example to use a command present in the command manifest",
}
}
func rejectUnknownFlag(ex skillscan.Example, commandPath, flag string) report.Diagnostic {
return report.Diagnostic{
Rule: "skill_command_reference",
Action: report.ActionReject,
File: ex.SourceFile,
Line: ex.Line,
Message: fmt.Sprintf("example references unknown flag --%s on %s", flag, commandPath),
Suggestion: "update the example flag or add the flag to the command implementation",
}
}