mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
527 lines
14 KiB
Go
527 lines
14 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package semantic
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/qualitygate/facts"
|
|
"github.com/larksuite/cli/internal/qualitygate/report"
|
|
)
|
|
|
|
type InputView struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
ChangedSummary ChangedSummary `json:"changed_summary"`
|
|
RuleSummary []RuleSummaryItem `json:"rule_summary,omitempty"`
|
|
Commands []CommandInput `json:"commands,omitempty"`
|
|
Skills []SkillInput `json:"skills,omitempty"`
|
|
SkillQuality []SkillQualityInput `json:"skill_quality,omitempty"`
|
|
Errors []ErrorInput `json:"errors,omitempty"`
|
|
Outputs []OutputInput `json:"outputs,omitempty"`
|
|
Examples []ExampleInput `json:"examples,omitempty"`
|
|
Diagnostics []facts.DiagnosticFact `json:"diagnostics,omitempty"`
|
|
}
|
|
|
|
type ChangedSummary struct {
|
|
Commands int `json:"commands,omitempty"`
|
|
Skills int `json:"skills,omitempty"`
|
|
SkillQuality int `json:"skill_quality,omitempty"`
|
|
Errors int `json:"errors,omitempty"`
|
|
Outputs int `json:"outputs,omitempty"`
|
|
Examples int `json:"examples,omitempty"`
|
|
Domains []string `json:"domains,omitempty"`
|
|
Sources []string `json:"sources,omitempty"`
|
|
}
|
|
|
|
type RuleSummaryItem struct {
|
|
Rule string `json:"rule"`
|
|
Action report.Action `json:"action"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type CommandInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
facts.CommandFact
|
|
}
|
|
|
|
func (i CommandInput) ref() string { return i.FactRef }
|
|
|
|
type SkillInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
facts.SkillFact
|
|
}
|
|
|
|
func (i SkillInput) ref() string { return i.FactRef }
|
|
|
|
type SkillQualityInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
facts.SkillQualityFact
|
|
}
|
|
|
|
type ErrorInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
facts.ErrorFact
|
|
}
|
|
|
|
func (i ErrorInput) ref() string { return i.FactRef }
|
|
|
|
type OutputInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
Command string `json:"command"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Changed bool `json:"changed,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
IsList bool `json:"is_list"`
|
|
HasDefaultLimit bool `json:"has_default_limit"`
|
|
HasDecisionField bool `json:"has_decision_field"`
|
|
}
|
|
|
|
func (i OutputInput) ref() string { return i.FactRef }
|
|
|
|
type ExampleInput struct {
|
|
FactRef string `json:"fact_ref"`
|
|
facts.CommandExample
|
|
}
|
|
|
|
func BuildInputView(f facts.Facts) InputView {
|
|
selected := newInputSelection(f)
|
|
selected.addChangedReviewCandidates()
|
|
|
|
var viewDiagnostics []facts.DiagnosticFact
|
|
for _, diag := range f.Diagnostics {
|
|
if !semanticDiagnosticRule(diag.Rule) {
|
|
continue
|
|
}
|
|
context := selected.diagnosticContext(diag)
|
|
if !includeDiagnosticInView(diag, selected, context) {
|
|
continue
|
|
}
|
|
viewDiagnostics = append(viewDiagnostics, diag)
|
|
selected.merge(context)
|
|
}
|
|
|
|
return InputView{
|
|
SchemaVersion: f.SchemaVersion,
|
|
ChangedSummary: changedSummary(f),
|
|
RuleSummary: ruleSummary(f.Diagnostics),
|
|
Commands: selected.commandInputs(),
|
|
Skills: selected.skillInputs(),
|
|
SkillQuality: selected.skillQualityInputs(),
|
|
Errors: selected.errorInputs(),
|
|
Outputs: selected.outputInputs(),
|
|
Examples: selected.exampleInputs(),
|
|
Diagnostics: viewDiagnostics,
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addChangedReviewCandidates() {
|
|
for i, cmd := range s.f.Commands {
|
|
if cmd.Changed && commandReviewCandidate(cmd) {
|
|
s.commands[i] = true
|
|
}
|
|
}
|
|
for i, skill := range s.f.Skills {
|
|
if skill.Changed && skillReviewCandidate(skill) {
|
|
s.skills[i] = true
|
|
}
|
|
}
|
|
for i, errFact := range s.f.Errors {
|
|
if errFact.Changed && errorReviewCandidate(errFact) {
|
|
s.errors[i] = true
|
|
}
|
|
}
|
|
for i, output := range s.f.Outputs {
|
|
if output.Changed && outputReviewCandidate(output) {
|
|
s.outputs[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func commandReviewCandidate(cmd facts.CommandFact) bool {
|
|
return cmd.NameConflictsExisting || cmd.FlagAliasConflict
|
|
}
|
|
|
|
func skillReviewCandidate(skill facts.SkillFact) bool {
|
|
return skill.ReferencesInvalidCommand
|
|
}
|
|
|
|
func errorReviewCandidate(errFact facts.ErrorFact) bool {
|
|
return errFact.Boundary && errFact.RequiredHint && errFact.HintActionCount == 0
|
|
}
|
|
|
|
func outputReviewCandidate(_ facts.OutputFact) bool {
|
|
// default_output is observe-first in the current rollout; reject diagnostics add exact output context.
|
|
return false
|
|
}
|
|
|
|
type inputSelection struct {
|
|
f facts.Facts
|
|
commands []bool
|
|
skills []bool
|
|
skillQuality []bool
|
|
errors []bool
|
|
outputs []bool
|
|
examples []bool
|
|
}
|
|
|
|
func newInputSelection(f facts.Facts) *inputSelection {
|
|
return &inputSelection{
|
|
f: f,
|
|
commands: make([]bool, len(f.Commands)),
|
|
skills: make([]bool, len(f.Skills)),
|
|
skillQuality: make([]bool, len(f.SkillQuality)),
|
|
errors: make([]bool, len(f.Errors)),
|
|
outputs: make([]bool, len(f.Outputs)),
|
|
examples: make([]bool, len(f.Examples)),
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) diagnosticContext(diag facts.DiagnosticFact) *inputSelection {
|
|
out := newInputSelection(s.f)
|
|
switch {
|
|
case diag.Rule == "command_naming" || diag.Rule == "flag_naming":
|
|
s.addDiagnosticCommands(out, diag)
|
|
case strings.HasPrefix(diag.Rule, "default_output"):
|
|
s.addDiagnosticOutputs(out, diag)
|
|
case strings.HasPrefix(diag.Rule, "skill_"):
|
|
s.addDiagnosticSkills(out, diag)
|
|
s.addDiagnosticSkillQuality(out, diag)
|
|
s.addDiagnosticExamples(out, diag)
|
|
case strings.HasPrefix(diag.Rule, "example_dry_run"):
|
|
s.addDiagnosticExamples(out, diag)
|
|
case diag.Rule == "no_bare_helper_error":
|
|
s.addDiagnosticErrors(out, diag)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticCommands(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, cmd := range s.f.Commands {
|
|
if diagnosticCommandMatches(diag, cmd.Path, cmd.CanonicalPath) ||
|
|
diagnosticMentions(diag, cmd.Path) ||
|
|
diagnosticMentions(diag, cmd.CanonicalPath) {
|
|
out.commands[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticSkills(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, skill := range s.f.Skills {
|
|
if diagnosticLocationMatches(diag.File, diag.Line, skill.SourceFile, skill.Line) ||
|
|
diagnosticCommandMatches(diag, skill.CommandPath) ||
|
|
diagnosticMentions(diag, skill.CommandPath) {
|
|
out.skills[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticSkillQuality(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, skill := range s.f.SkillQuality {
|
|
if samePath(diag.File, skill.SourceFile) {
|
|
out.skillQuality[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticErrors(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, errFact := range s.f.Errors {
|
|
if diagnosticLocationMatches(diag.File, diag.Line, errFact.File, errFact.Line) ||
|
|
diagnosticCommandMatches(diag, errFact.CommandPath, errFact.Command) ||
|
|
diagnosticMentions(diag, errFact.CommandPath) ||
|
|
diagnosticMentions(diag, errFact.Command) {
|
|
out.errors[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticOutputs(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, output := range s.f.Outputs {
|
|
if diagnosticCommandMatches(diag, output.Command) ||
|
|
diagnosticMentions(diag, output.Command) {
|
|
out.outputs[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *inputSelection) addDiagnosticExamples(out *inputSelection, diag facts.DiagnosticFact) {
|
|
for i, example := range s.f.Examples {
|
|
if diagnosticLocationMatches(diag.File, diag.Line, example.SourceFile, example.Line) ||
|
|
diagnosticCommandMatches(diag, example.CommandPath) ||
|
|
diagnosticMentions(diag, example.CommandPath) {
|
|
out.examples[i] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func includeDiagnosticInView(diag facts.DiagnosticFact, selected, context *inputSelection) bool {
|
|
if diag.Action == report.ActionReject {
|
|
return true
|
|
}
|
|
return selected.intersects(context)
|
|
}
|
|
|
|
func (s *inputSelection) merge(other *inputSelection) {
|
|
mergeSelections(s.commands, other.commands)
|
|
mergeSelections(s.skills, other.skills)
|
|
mergeSelections(s.skillQuality, other.skillQuality)
|
|
mergeSelections(s.errors, other.errors)
|
|
mergeSelections(s.outputs, other.outputs)
|
|
mergeSelections(s.examples, other.examples)
|
|
}
|
|
|
|
func (s *inputSelection) intersects(other *inputSelection) bool {
|
|
return selectionsIntersect(s.commands, other.commands) ||
|
|
selectionsIntersect(s.skills, other.skills) ||
|
|
selectionsIntersect(s.skillQuality, other.skillQuality) ||
|
|
selectionsIntersect(s.errors, other.errors) ||
|
|
selectionsIntersect(s.outputs, other.outputs) ||
|
|
selectionsIntersect(s.examples, other.examples)
|
|
}
|
|
|
|
func (s *inputSelection) commandInputs() []CommandInput {
|
|
out := make([]CommandInput, 0, countSelected(s.commands))
|
|
for i, ok := range s.commands {
|
|
if ok {
|
|
out = append(out, CommandInput{FactRef: factRef("commands", i), CommandFact: s.f.Commands[i]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) skillInputs() []SkillInput {
|
|
out := make([]SkillInput, 0, countSelected(s.skills))
|
|
for i, ok := range s.skills {
|
|
if ok {
|
|
out = append(out, SkillInput{FactRef: factRef("skills", i), SkillFact: s.f.Skills[i]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) skillQualityInputs() []SkillQualityInput {
|
|
out := make([]SkillQualityInput, 0, countSelected(s.skillQuality))
|
|
for i, ok := range s.skillQuality {
|
|
if ok {
|
|
out = append(out, SkillQualityInput{FactRef: factRef("skill_quality", i), SkillQualityFact: s.f.SkillQuality[i]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) errorInputs() []ErrorInput {
|
|
out := make([]ErrorInput, 0, countSelected(s.errors))
|
|
for i, ok := range s.errors {
|
|
if ok {
|
|
out = append(out, ErrorInput{FactRef: factRef("errors", i), ErrorFact: s.f.Errors[i]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) outputInputs() []OutputInput {
|
|
out := make([]OutputInput, 0, countSelected(s.outputs))
|
|
for i, ok := range s.outputs {
|
|
if ok {
|
|
output := s.f.Outputs[i]
|
|
out = append(out, OutputInput{
|
|
FactRef: factRef("outputs", i),
|
|
Command: output.Command,
|
|
Domain: output.Domain,
|
|
Changed: output.Changed,
|
|
Source: output.Source,
|
|
IsList: output.IsList,
|
|
HasDefaultLimit: output.HasDefaultLimit,
|
|
HasDecisionField: output.HasDecisionField,
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *inputSelection) exampleInputs() []ExampleInput {
|
|
out := make([]ExampleInput, 0, countSelected(s.examples))
|
|
for i, ok := range s.examples {
|
|
if ok {
|
|
out = append(out, ExampleInput{FactRef: factRef("examples", i), CommandExample: s.f.Examples[i]})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func changedSummary(f facts.Facts) ChangedSummary {
|
|
domains := map[string]bool{}
|
|
sources := map[string]bool{}
|
|
var out ChangedSummary
|
|
for _, cmd := range f.Commands {
|
|
if !cmd.Changed {
|
|
continue
|
|
}
|
|
out.Commands++
|
|
addNonEmpty(domains, cmd.Domain)
|
|
addNonEmpty(sources, cmd.Source)
|
|
}
|
|
for _, skill := range f.Skills {
|
|
if !skill.Changed {
|
|
continue
|
|
}
|
|
out.Skills++
|
|
addNonEmpty(domains, skill.Domain)
|
|
addNonEmpty(sources, skill.Source)
|
|
}
|
|
for _, skill := range f.SkillQuality {
|
|
if !skill.Changed {
|
|
continue
|
|
}
|
|
out.SkillQuality++
|
|
addNonEmpty(domains, skill.Domain)
|
|
}
|
|
for _, errFact := range f.Errors {
|
|
if !errFact.Changed {
|
|
continue
|
|
}
|
|
out.Errors++
|
|
addNonEmpty(domains, errFact.Domain)
|
|
addNonEmpty(sources, errFact.Source)
|
|
}
|
|
for _, output := range f.Outputs {
|
|
if !output.Changed {
|
|
continue
|
|
}
|
|
out.Outputs++
|
|
addNonEmpty(domains, output.Domain)
|
|
addNonEmpty(sources, output.Source)
|
|
}
|
|
for _, example := range f.Examples {
|
|
if !example.Changed {
|
|
continue
|
|
}
|
|
out.Examples++
|
|
addNonEmpty(domains, example.Domain)
|
|
addNonEmpty(sources, example.Source)
|
|
}
|
|
out.Domains = sortedViewSetKeys(domains)
|
|
out.Sources = sortedViewSetKeys(sources)
|
|
return out
|
|
}
|
|
|
|
func ruleSummary(diags []facts.DiagnosticFact) []RuleSummaryItem {
|
|
counts := map[string]int{}
|
|
actions := map[string]report.Action{}
|
|
for _, diag := range diags {
|
|
key := string(diag.Action) + "\x00" + diag.Rule
|
|
counts[key]++
|
|
actions[key] = diag.Action
|
|
}
|
|
keys := sortedKeysInt(counts)
|
|
out := make([]RuleSummaryItem, 0, len(keys))
|
|
for _, key := range keys {
|
|
_, rule, _ := strings.Cut(key, "\x00")
|
|
out = append(out, RuleSummaryItem{
|
|
Rule: rule,
|
|
Action: actions[key],
|
|
Count: counts[key],
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func semanticDiagnosticRule(rule string) bool {
|
|
return rule == "command_naming" ||
|
|
rule == "flag_naming" ||
|
|
strings.HasPrefix(rule, "default_output") ||
|
|
strings.HasPrefix(rule, "skill_") ||
|
|
strings.HasPrefix(rule, "example_dry_run") ||
|
|
rule == "no_bare_helper_error"
|
|
}
|
|
|
|
func diagnosticCommandMatches(diag facts.DiagnosticFact, values ...string) bool {
|
|
if diag.CommandPath == "" {
|
|
return false
|
|
}
|
|
for _, value := range values {
|
|
if value != "" && diag.CommandPath == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func diagnosticLocationMatches(diagFile string, diagLine int, factFile string, factLine int) bool {
|
|
if !samePath(diagFile, factFile) {
|
|
return false
|
|
}
|
|
return diagLine == 0 || factLine == 0 || diagLine == factLine
|
|
}
|
|
|
|
func diagnosticMentions(diag facts.DiagnosticFact, value string) bool {
|
|
if value == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(diag.Message, value) ||
|
|
strings.Contains(diag.Suggestion, value)
|
|
}
|
|
|
|
func samePath(a, b string) bool {
|
|
return normalizeViewPath(a) == normalizeViewPath(b)
|
|
}
|
|
|
|
func normalizeViewPath(path string) string {
|
|
return strings.TrimPrefix(strings.ReplaceAll(path, "\\", "/"), "./")
|
|
}
|
|
|
|
func factRef(kind string, idx int) string {
|
|
return fmt.Sprintf("facts.%s[%d]", kind, idx)
|
|
}
|
|
|
|
func addNonEmpty(set map[string]bool, value string) {
|
|
if value != "" {
|
|
set[value] = true
|
|
}
|
|
}
|
|
|
|
func countSelected(items []bool) int {
|
|
var count int
|
|
for _, item := range items {
|
|
if item {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func mergeSelections(dst, src []bool) {
|
|
for i := range dst {
|
|
dst[i] = dst[i] || src[i]
|
|
}
|
|
}
|
|
|
|
func selectionsIntersect(a, b []bool) bool {
|
|
for i := range a {
|
|
if a[i] && b[i] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sortedViewSetKeys(set map[string]bool) []string {
|
|
keys := make([]string, 0, len(set))
|
|
for key := range set {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func sortedKeysInt(set map[string]int) []string {
|
|
keys := make([]string, 0, len(set))
|
|
for key := range set {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|