Skip to content
Closed
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/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/)
- 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/|shortcuts/wiki/)
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/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/|shortcuts/common/mcp_client\.go)
- 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/wiki/|shortcuts/common/mcp_client\.go)
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/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/minutes/|shortcuts/okr/|shortcuts/task/|shortcuts/vc/|shortcuts/whiteboard/)
- 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/wiki/)
text: errs-no-legacy-helper
linters:
- forbidigo
Expand Down
6 changes: 6 additions & 0 deletions lint/errscontract/rule_no_legacy_common_helper_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ var migratedCommonHelperPaths = []string{
"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/wiki/",
}

const commonImportPath = "github.com/larksuite/cli/shortcuts/common"
Expand Down
7 changes: 6 additions & 1 deletion lint/errscontract/rule_no_legacy_envelope_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ var migratedEnvelopePaths = []string{
"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/im/",
"shortcuts/wiki/",
}

// legacyOutputImportPath is the import path of the package that declares the
Expand Down
36 changes: 36 additions & 0 deletions lint/errscontract/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,11 +944,19 @@ func TestCheckNoLegacyCommonHelperCall_RejectsLegacyHelpersOnMigratedPath(t *tes
"HandleApiResult",
}
paths := []string{
"shortcuts/calendar/calendar_create.go",
"shortcuts/contact/contact_search_user.go",
"shortcuts/doc/docs_fetch_v2.go",
"shortcuts/drive/drive_search.go",
"shortcuts/im/im_messages_send.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",
"shortcuts/task/task_update.go",
"shortcuts/whiteboard/whiteboard_query.go",
"shortcuts/wiki/wiki_node_get.go",
}
for _, path := range paths {
for _, helper := range helpers {
Expand Down Expand Up @@ -997,6 +1005,34 @@ func boom() {
}
}

func TestCheckNoLegacyCommonHelperCall_CoversCCMPathsWithAliasAndFunctionValue(t *testing.T) {
paths := []string{
"shortcuts/doc/docs_fetch_v2.go",
"shortcuts/markdown/markdown_fetch.go",
"shortcuts/sheets/helpers.go",
"shortcuts/slides/slides_create.go",
"shortcuts/wiki/wiki_node_get.go",
}
src := `package migrated

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

func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
for _, path := range paths {
t.Run(path, func(t *testing.T) {
v := CheckNoLegacyCommonHelperCall(path, src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on %s, got %d: %+v", path, 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 legacy category (e.g. "io", "api_error") change
// their envelope's type field.
func WrapSaveErrorTyped(err error) error {
if err == nil {
return nil
Expand Down
30 changes: 16 additions & 14 deletions shortcuts/doc/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"regexp"
"runtime"
"strings"

"github.com/larksuite/cli/errs"
)

// readClipboardImageBytes reads the current clipboard image and returns the
Expand All @@ -35,13 +37,13 @@
case "linux":
data, err = readClipboardLinux()
default:
return nil, fmt.Errorf("clipboard image upload is not supported on %s", runtime.GOOS)
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image upload is not supported on %s", runtime.GOOS)

Check warning on line 40 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L40

Added line #L40 was not covered by tests
}
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("clipboard contains no image data")
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")

Check warning on line 46 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L46

Added line #L46 was not covered by tests
}
return data, nil
}
Expand Down Expand Up @@ -91,9 +93,9 @@
}

if stderrText != "" {
return nil, fmt.Errorf("clipboard contains no image data (osascript: %s)", stderrText)
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (osascript: %s)", stderrText)

Check warning on line 96 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L96

Added line #L96 was not covered by tests
}
return nil, fmt.Errorf("clipboard contains no image data")
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")

Check warning on line 98 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L98

Added line #L98 was not covered by tests
}

// runOsascript invokes osascript with a single AppleScript expression and
Expand Down Expand Up @@ -188,14 +190,14 @@
// decodeHex decodes an uppercase hex string (as produced by osascript) to bytes.
func decodeHex(h string) ([]byte, error) {
if len(h)%2 != 0 {
return nil, fmt.Errorf("odd hex length")
return nil, fmt.Errorf("odd hex length") //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
}
b := make([]byte, len(h)/2)
for i := 0; i < len(h); i += 2 {
hi := hexVal(h[i])
lo := hexVal(h[i+1])
if hi < 0 || lo < 0 {
return nil, fmt.Errorf("invalid hex char at %d", i)
return nil, fmt.Errorf("invalid hex char at %d", i) //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
}
b[i/2] = byte(hi<<4 | lo)
}
Expand Down Expand Up @@ -237,12 +239,12 @@
if msg == "" {
msg = err.Error()
}
return nil, fmt.Errorf("clipboard read failed (%s)", msg)
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard read failed (%s)", msg)

Check warning on line 242 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L242

Added line #L242 was not covered by tests
}
b64 := strings.TrimSpace(string(out))
data, decErr := base64.StdEncoding.DecodeString(b64)
if decErr != nil {
return nil, fmt.Errorf("clipboard image decode failed: %w", decErr)
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image decode failed: %s", decErr).WithCause(decErr)

Check warning on line 247 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L247

Added line #L247 was not covered by tests
}
return data, nil
}
Expand Down Expand Up @@ -325,15 +327,15 @@
foundTool = true
out, err := exec.Command(t.name, t.args...).Output()
if err != nil {
lastErr = fmt.Errorf("clipboard image read failed via %s: %w", t.name, err)
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image read failed via %s: %s", t.name, err).WithCause(err)

Check warning on line 330 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L330

Added line #L330 was not covered by tests
continue
}
if len(out) == 0 {
lastErr = fmt.Errorf("clipboard contains no image data (%s returned empty output)", t.name)
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (%s returned empty output)", t.name)

Check warning on line 334 in shortcuts/doc/clipboard.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/clipboard.go#L334

Added line #L334 was not covered by tests
continue
}
if t.validatePNG && !hasPNGMagic(out) {
lastErr = fmt.Errorf("clipboard contains no PNG image data (%s output is not a PNG)", t.name)
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no PNG image data (%s output is not a PNG)", t.name)
continue
}
return out, nil
Expand All @@ -342,8 +344,8 @@
if foundTool && lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf(
"clipboard image read failed: no supported tool found. " +
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager " +
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition,
"clipboard image read failed: no supported tool found. "+
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager "+
"(apt, dnf, pacman, apk, brew, etc.).")
}
16 changes: 16 additions & 0 deletions shortcuts/doc/doc_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package doc

import "github.com/larksuite/cli/errs"

// wrapDocNetworkErr returns err unchanged when it is already a typed errs.*
// error (preserving its subtype / code / log_id from the runtime boundary),
// and only wraps a raw, unclassified error as a transport-level network error.
func wrapDocNetworkErr(err error, format string, args ...any) error {
if _, ok := errs.ProblemOf(err); ok {
return err
}
return errs.NewNetworkError(errs.SubtypeNetworkTransport, format, args...).WithCause(err)

Check warning on line 15 in shortcuts/doc/doc_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/doc_errors.go#L15

Added line #L15 was not covered by tests
}
14 changes: 7 additions & 7 deletions shortcuts/doc/doc_media_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

larkcore "github.com/larksuite/oapi-sdk-go/v3/core"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
Expand Down Expand Up @@ -51,10 +51,10 @@
overwrite := runtime.Bool("overwrite")

if err := validate.ResourceName(token, "--token"); err != nil {
return output.ErrValidation("%s", err)
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--token")

Check warning on line 54 in shortcuts/doc/doc_media_download.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/doc_media_download.go#L54

Added line #L54 was not covered by tests
}
if _, err := runtime.ResolveSavePath(outputPath); err != nil {
return output.ErrValidation("unsafe output path: %s", err)
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)

Check warning on line 57 in shortcuts/doc/doc_media_download.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/doc_media_download.go#L57

Added line #L57 was not covered by tests
}

fmt.Fprintf(runtime.IO().ErrOut, "Downloading: %s %s\n", mediaType, common.MaskToken(token))
Expand All @@ -73,7 +73,7 @@
ApiPath: apiPath,
})
if err != nil {
return output.ErrNetwork("download failed: %v", err)
return wrapDocNetworkErr(err, "download failed: %v", err)
}
defer resp.Body.Close()

Expand All @@ -86,14 +86,14 @@
// Validate final path after extension append
if finalPath != outputPath {
if _, err := runtime.ResolveSavePath(finalPath); err != nil {
return output.ErrValidation("unsafe output path: %s", err)
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)

Check warning on line 89 in shortcuts/doc/doc_media_download.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/doc_media_download.go#L89

Added line #L89 was not covered by tests
}
}

// Overwrite check on final path (after extension detection)
if !overwrite {
if _, statErr := runtime.FileIO().Stat(finalPath); statErr == nil {
return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", finalPath)
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "output file already exists: %s (use --overwrite to replace)", finalPath).WithParam("--output")
}
}

Expand All @@ -102,7 +102,7 @@
ContentLength: resp.ContentLength,
}, resp.Body)
if err != nil {
return common.WrapSaveErrorByCategory(err, "io")
return common.WrapSaveErrorTyped(err)

Check warning on line 105 in shortcuts/doc/doc_media_download.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/doc_media_download.go#L105

Added line #L105 was not covered by tests
}

savedPath, _ := runtime.ResolveSavePath(finalPath)
Expand Down
Loading
Loading