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
6 changes: 3 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,20 @@ linters:
- forbidigo
# errs-typed-only enforced on paths already migrated to errs.NewXxxError.
# Add a path when its migration is complete.
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|internal/event/consume/|cmd/event/|events/|shortcuts/event/)
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|internal/event/consume/|cmd/event/|events/|shortcuts/event/)
text: errs-typed-only
linters:
- forbidigo
# errs-no-bare-wrap enforced on paths fully migrated to typed final
# errors. Scoped separately from errs-typed-only because cmd/auth/,
# cmd/config/ still have residual fmt.Errorf and must not be caught.
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|shortcuts/common/mcp_client\.go|cmd/event/|events/|shortcuts/event/)
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|shortcuts/common/mcp_client\.go|cmd/event/|events/|shortcuts/event/)
text: errs-no-bare-wrap
linters:
- forbidigo
# errs-no-legacy-helper enforced on domains whose shared validation/save
# helpers have migrated to typed final errors.
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|cmd/event/|events/|shortcuts/event/)
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/contact/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/minutes/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|cmd/event/|events/|shortcuts/event/)
text: errs-no-legacy-helper
linters:
- forbidigo
Expand Down
1 change: 1 addition & 0 deletions lint/errscontract/rule_no_legacy_common_helper_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var migratedCommonHelperPaths = []string{
"shortcuts/drive/",
"shortcuts/event/",
"shortcuts/mail/",
"shortcuts/markdown/",
"shortcuts/minutes/",
"shortcuts/okr/",
"shortcuts/sheets/",
Expand Down
1 change: 1 addition & 0 deletions lint/errscontract/rule_no_legacy_envelope_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var migratedEnvelopePaths = []string{
"shortcuts/drive/",
"shortcuts/event/",
"shortcuts/mail/",
"shortcuts/markdown/",
"shortcuts/minutes/",
"shortcuts/okr/",
"shortcuts/sheets/",
Expand Down
19 changes: 19 additions & 0 deletions lint/errscontract/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ func boom() error {

func TestCheckNoLegacyEnvelopeLiteral_RejectsExitErrorLiteralOnMigratedShortcutPaths(t *testing.T) {
for _, path := range []string{
"shortcuts/markdown/markdown_fetch.go",
"shortcuts/okr/okr_image_upload.go",
"shortcuts/task/task_update.go",
"shortcuts/whiteboard/whiteboard_update.go",
Expand Down Expand Up @@ -953,6 +954,7 @@ func TestCheckNoLegacyCommonHelperCall_RejectsLegacyHelpersOnMigratedPath(t *tes
"shortcuts/doc/docs_fetch_v2.go",
"shortcuts/drive/drive_search.go",
"shortcuts/mail/mail_send.go",
"shortcuts/markdown/markdown_fetch.go",
"shortcuts/okr/okr_progress_create.go",
"shortcuts/sheets/helpers.go",
"shortcuts/slides/slides_create.go",
Expand Down Expand Up @@ -1057,6 +1059,23 @@ func boom() {
}
}

func TestCheckNoLegacyCommonHelperCall_CoversMarkdownPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated

import c "github.com/larksuite/cli/shortcuts/common"

func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/markdown/markdown_fetch.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on markdown path, got %d: %+v", len(v), v)
}
}

func TestCheckNoLegacyCommonHelperCall_AllowsNonMigratedPath(t *testing.T) {
src := `package contact

Expand Down
26 changes: 3 additions & 23 deletions shortcuts/common/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,30 +676,10 @@ func WrapInputStatErrorTyped(err error, readMsg ...string) error {
WithCause(err)
}

// WrapSaveErrorByCategory maps a FileIO.Save error to structured output errors,
// using standardized messages and the given error category (e.g. "api_error", "io").
// Path validation errors always use ErrValidation (exit code 2).
//
// Deprecated: use WrapSaveErrorTyped for typed error envelopes.
func WrapSaveErrorByCategory(err error, category string) error {
if err == nil {
return nil
}
var me *fileio.MkdirError
switch {
case errors.Is(err, fileio.ErrPathValidation):
return output.ErrValidation("unsafe output path: %s", err)
case errors.As(err, &me):
return output.Errorf(output.ExitInternal, category, "cannot create parent directory: %s", err)
default:
return output.Errorf(output.ExitInternal, category, "cannot create file: %s", err)
}
}

// WrapSaveErrorTyped maps a FileIO.Save error to typed validation/internal errors.
// Unlike WrapSaveErrorByCategory, non-path failures always emit the canonical
// "internal" wire type: call sites migrating from a custom category
// (e.g. "io", "api_error") change their envelope's type field.
// Non-path failures always emit the canonical "internal" wire type; call sites
// migrating from a custom category (e.g. "io", "api_error") change their
// envelope's type field.
func WrapSaveErrorTyped(err error) error {
if err == nil {
return nil
Expand Down
54 changes: 21 additions & 33 deletions shortcuts/markdown/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -19,7 +18,6 @@

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/client"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
Expand Down Expand Up @@ -85,16 +83,18 @@
func validateMarkdownSpec(runtime *common.RuntimeContext, spec markdownUploadSpec, requireName bool) error {
switch {
case spec.ContentSet && spec.FileSet:
return common.FlagErrorf("--content and --file are mutually exclusive")
return markdownValidationError("--content and --file are mutually exclusive").
WithParams(markdownInvalidParam("--content", "mutually exclusive"), markdownInvalidParam("--file", "mutually exclusive"))
case !spec.ContentSet && !spec.FileSet:
return common.FlagErrorf("specify exactly one of --content or --file")
return markdownValidationError("specify exactly one of --content or --file").
WithParams(markdownInvalidParam("--content", "required; specify exactly one"), markdownInvalidParam("--file", "required; specify exactly one"))
}

if markdownFlagExplicitlyEmpty(runtime, "folder-token") {
return common.FlagErrorf("--folder-token cannot be empty; omit it to upload into Drive root folder")
return markdownValidationParamError("--folder-token", "--folder-token cannot be empty; omit it to upload into Drive root folder")
}
if markdownFlagExplicitlyEmpty(runtime, "wiki-token") {
return common.FlagErrorf("--wiki-token cannot be empty; provide a valid wiki node token or omit the flag entirely")
return markdownValidationParamError("--wiki-token", "--wiki-token cannot be empty; provide a valid wiki node token or omit the flag entirely")
}
targets := 0
if spec.FolderToken != "" {
Expand All @@ -104,22 +104,23 @@
targets++
}
if targets > 1 {
return common.FlagErrorf("--folder-token and --wiki-token are mutually exclusive")
return markdownValidationError("--folder-token and --wiki-token are mutually exclusive").
WithParams(markdownInvalidParam("--folder-token", "mutually exclusive"), markdownInvalidParam("--wiki-token", "mutually exclusive"))
}
if spec.FolderToken != "" {
if err := validate.ResourceName(spec.FolderToken, "--folder-token"); err != nil {
return output.ErrValidation("%s", err)
return markdownValidationParamError("--folder-token", "%s", err).WithCause(err)
}
}
if spec.WikiToken != "" {
if err := validate.ResourceName(spec.WikiToken, "--wiki-token"); err != nil {
return output.ErrValidation("%s", err)
return markdownValidationParamError("--wiki-token", "%s", err).WithCause(err)
}
}

if requireName && spec.ContentSet {
if strings.TrimSpace(spec.FileName) == "" {
return common.FlagErrorf("--name is required when using --content")
return markdownValidationParamError("--name", "--name is required when using --content")
}
if err := validateMarkdownFileName(spec.FileName, "--name"); err != nil {
return err
Expand All @@ -128,10 +129,10 @@

if spec.FileSet {
if strings.TrimSpace(spec.FilePath) == "" {
return common.FlagErrorf("--file cannot be empty")
return markdownValidationParamError("--file", "--file cannot be empty")
}
if _, err := validate.SafeInputPath(spec.FilePath); err != nil {
return output.ErrValidation("unsafe file path: %s", err)
return markdownValidationParamError("--file", "unsafe file path: %s", err).WithCause(err)

Check warning on line 135 in shortcuts/markdown/helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/helpers.go#L135

Added line #L135 was not covered by tests
}
if err := validateMarkdownFileName(filepath.Base(spec.FilePath), "--file"); err != nil {
return err
Expand All @@ -154,10 +155,10 @@
func validateMarkdownFileName(name, flagName string) error {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return common.FlagErrorf("%s cannot be empty", flagName)
return markdownValidationParamError(flagName, "%s cannot be empty", flagName)

Check warning on line 158 in shortcuts/markdown/helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/helpers.go#L158

Added line #L158 was not covered by tests
}
if !strings.HasSuffix(strings.ToLower(trimmed), ".md") {
return common.FlagErrorf("%s must end with .md", flagName)
return markdownValidationParamError(flagName, "%s must end with .md", flagName)
}
return nil
}
Expand Down Expand Up @@ -201,22 +202,9 @@
return resp, nil
}

func wrapMarkdownDownloadError(err error) error {
// Preserve any already-classified error: legacy *output.ExitError or any
// typed errs.* error. Only un-classified errors get wrapped as network.
var exitErr *output.ExitError
if errors.As(err, &exitErr) {
return err
}
if _, ok := errs.ProblemOf(err); ok {
return err
}
return output.ErrNetwork("download failed: %s", err)
}

func validateNonEmptyMarkdownSize(size int64) error {
if size == 0 {
return output.ErrValidation("%s", markdownEmptyContentError)
return markdownValidationError("%s", markdownEmptyContentError)
}
return nil
}
Expand All @@ -227,12 +215,12 @@
size = int64(len(spec.Content))
} else {
if strings.TrimSpace(spec.FilePath) == "" {
return 0, common.FlagErrorf("--file cannot be empty")
return 0, markdownValidationParamError("--file", "--file cannot be empty")

Check warning on line 218 in shortcuts/markdown/helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/helpers.go#L218

Added line #L218 was not covered by tests
}

info, err := runtime.FileIO().Stat(spec.FilePath)
if err != nil {
return 0, common.WrapInputStatError(err)
return 0, withMarkdownFileParam(common.WrapInputStatErrorTyped(err), "--file")
}
size = info.Size()
}
Expand Down Expand Up @@ -424,7 +412,7 @@
return withMarkdownUploadRetryResult(runtime, markdownUploadAllAction, func() (markdownUploadResult, error) {
fileReader, err := openReader()
if err != nil {
return markdownUploadResult{}, common.WrapInputStatErrorTyped(err)
return markdownUploadResult{}, withMarkdownFileParam(common.WrapInputStatErrorTyped(err), "--file")

Check warning on line 415 in shortcuts/markdown/helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/helpers.go#L415

Added line #L415 was not covered by tests
}
defer fileReader.Close()

Expand Down Expand Up @@ -491,7 +479,7 @@

fileReader, err := openReader()
if err != nil {
return markdownUploadResult{}, common.WrapInputStatErrorTyped(err)
return markdownUploadResult{}, withMarkdownFileParam(common.WrapInputStatErrorTyped(err), "--file")
}
defer fileReader.Close()

Expand Down Expand Up @@ -563,7 +551,7 @@

n, readErr := io.ReadFull(fileReader, buffer[:int(chunkSize)])
if readErr != nil {
return output.ErrValidation("cannot read file: %s", readErr)
return markdownValidationParamError("--file", "cannot read file: %s", readErr).WithCause(readErr)

Check warning on line 554 in shortcuts/markdown/helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/helpers.go#L554

Added line #L554 was not covered by tests
}

fd := larkcore.NewFormdata()
Expand Down
36 changes: 20 additions & 16 deletions shortcuts/markdown/markdown_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import (
"context"
"errors"
"fmt"
"io"
"regexp"
Expand All @@ -14,6 +13,7 @@

"github.com/sergi/go-diff/diffmatchpatch"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
Expand Down Expand Up @@ -65,7 +65,7 @@

func validateMarkdownDiffSpec(runtime *common.RuntimeContext, spec markdownDiffSpec) error {
if err := validate.ResourceName(spec.FileToken, "--file-token"); err != nil {
return output.ErrValidation("%s", err)
return markdownValidationParamError("--file-token", "%s", err).WithCause(err)

Check warning on line 68 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L68

Added line #L68 was not covered by tests
}
if spec.FromVersion != "" {
if err := validateMarkdownDiffVersionValue(spec.FromVersion, "--from-version"); err != nil {
Expand All @@ -79,40 +79,45 @@
}
if spec.FilePath != "" {
if _, err := validate.SafeInputPath(spec.FilePath); err != nil {
return output.ErrValidation("unsafe file path: %s", err)
return markdownValidationParamError("--file", "unsafe file path: %s", err).WithCause(err)

Check warning on line 82 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L82

Added line #L82 was not covered by tests
}
if err := validateMarkdownFileName(spec.FilePath, "--file"); err != nil {
return err
}
}
if spec.ContextLines < 0 {
return output.ErrValidation("--context-lines must be >= 0")
return markdownValidationParamError("--context-lines", "--context-lines must be >= 0")

Check warning on line 89 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L89

Added line #L89 was not covered by tests
}
if spec.Format != "" && spec.Format != "json" && spec.Format != "pretty" {
return output.ErrValidation("markdown +diff only supports --format json or pretty")
return markdownValidationParamError("--format", "markdown +diff only supports --format json or pretty")
}
if spec.FilePath == "" {
if spec.FromVersion == "" && spec.ToVersion == "" {
return common.FlagErrorf("specify --from-version, or both --from-version and --to-version, or use --file for remote vs local diff")
return markdownValidationError("specify --from-version, or both --from-version and --to-version, or use --file for remote vs local diff").
WithParams(
markdownInvalidParam("--from-version", "required; specify one"),
markdownInvalidParam("--to-version", "required; specify one"),
markdownInvalidParam("--file", "required; specify one"),
)
}
if spec.FromVersion == "" && spec.ToVersion != "" {
return common.FlagErrorf("--to-version requires --from-version")
return markdownValidationParamError("--to-version", "--to-version requires --from-version")
}
return nil
}
if spec.ToVersion != "" {
return common.FlagErrorf("--to-version is not supported together with --file")
return markdownValidationParamError("--to-version", "--to-version is not supported together with --file")

Check warning on line 109 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L109

Added line #L109 was not covered by tests
}
return nil
}

func validateMarkdownDiffVersionValue(value, flagName string) error {
value = strings.TrimSpace(value)
if value == "" {
return output.ErrValidation("%s cannot be empty", flagName)
return markdownValidationParamError(flagName, "%s cannot be empty", flagName)

Check warning on line 117 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L117

Added line #L117 was not covered by tests
}
if !markdownDiffVersionRe.MatchString(value) {
return output.ErrValidation("%s must be a numeric version string", flagName)
return markdownValidationParamError(flagName, "%s must be a numeric version string", flagName)

Check warning on line 120 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L120

Added line #L120 was not covered by tests
}
return nil
}
Expand Down Expand Up @@ -178,17 +183,16 @@
func readMarkdownLocalFile(runtime *common.RuntimeContext, filePath string) (string, error) {
f, err := runtime.FileIO().Open(filePath)
if err != nil {
return "", common.WrapInputStatError(err)
return "", withMarkdownFileParam(common.WrapInputStatErrorTyped(err), "--file")

Check warning on line 186 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L186

Added line #L186 was not covered by tests
}
defer f.Close()

payload, err := readMarkdownDiffPayload(f, "local Markdown file")
if err != nil {
var exitErr *output.ExitError
if errors.As(err, &exitErr) {
return "", err
if _, ok := errs.ProblemOf(err); ok {
return "", withMarkdownFileParam(err, "--file")
}
return "", output.ErrValidation("cannot read file: %s", err)
return "", markdownValidationParamError("--file", "cannot read file: %s", err).WithCause(err)

Check warning on line 195 in shortcuts/markdown/markdown_diff.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/markdown/markdown_diff.go#L195

Added line #L195 was not covered by tests
}
return string(payload), nil
}
Expand All @@ -199,7 +203,7 @@
return nil, err
}
if len(payload) > markdownDiffMaxContentBytes {
return nil, output.ErrValidation("%s exceeds %s markdown +diff content limit", source, common.FormatSize(markdownDiffMaxContentBytes))
return nil, markdownValidationError("%s exceeds %s markdown +diff content limit", source, common.FormatSize(markdownDiffMaxContentBytes))
}
return payload, nil
}
Expand Down
Loading
Loading