Skip to content

Commit 44949a5

Browse files
committed
feat(doc,im,markdown,sheets,slides,wiki): emit typed error envelopes across shortcut domains
Classify doc, markdown, sheets, slides, and wiki shortcut failures with typed errors so CLI users and automation receive more specific, actionable diagnostics. Keep the main migration scoped to the CCM shortcut domains and their error-contract guards. Preserve latest main's migrated IM guard by typing the new feed group shortcut validation and API error paths introduced upstream.
1 parent 5788a6c commit 44949a5

83 files changed

Lines changed: 1463 additions & 1080 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.golangci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,20 @@ linters:
7373
- forbidigo
7474
# errs-typed-only enforced on paths already migrated to errs.NewXxxError.
7575
# Add a path when its migration is complete.
76-
- 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/drive/|shortcuts/mail/|shortcuts/okr/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/im/)
76+
- 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/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/wiki/)
7777
text: errs-typed-only
7878
linters:
7979
- forbidigo
8080
# errs-no-bare-wrap enforced on paths fully migrated to typed final
8181
# errors. Scoped separately from errs-typed-only because cmd/auth/,
8282
# cmd/config/ still have residual fmt.Errorf and must not be caught.
83-
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/drive/|shortcuts/mail/|shortcuts/okr/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/im/|shortcuts/common/mcp_client\.go)
83+
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/wiki/|shortcuts/common/mcp_client\.go)
8484
text: errs-no-bare-wrap
8585
linters:
8686
- forbidigo
8787
# errs-no-legacy-helper enforced on domains whose shared validation/save
8888
# helpers have migrated to typed final errors.
89-
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/drive/|shortcuts/mail/|shortcuts/okr/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/im/)
89+
- path-except: (shortcuts/base/|shortcuts/calendar/|shortcuts/doc/|shortcuts/drive/|shortcuts/im/|shortcuts/mail/|shortcuts/markdown/|shortcuts/okr/|shortcuts/sheets/|shortcuts/slides/|shortcuts/task/|shortcuts/whiteboard/|shortcuts/wiki/)
9090
text: errs-no-legacy-helper
9191
linters:
9292
- forbidigo

lint/errscontract/rule_no_legacy_common_helper_call.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ import (
1717
var migratedCommonHelperPaths = []string{
1818
"shortcuts/base/",
1919
"shortcuts/calendar/",
20+
"shortcuts/doc/",
2021
"shortcuts/drive/",
22+
"shortcuts/im/",
2123
"shortcuts/mail/",
24+
"shortcuts/markdown/",
2225
"shortcuts/okr/",
26+
"shortcuts/sheets/",
27+
"shortcuts/slides/",
2328
"shortcuts/task/",
2429
"shortcuts/whiteboard/",
30+
"shortcuts/wiki/",
2531
}
2632

2733
const commonImportPath = "github.com/larksuite/cli/shortcuts/common"

lint/errscontract/rule_no_legacy_envelope_literal.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ import (
1818
var migratedEnvelopePaths = []string{
1919
"shortcuts/base/",
2020
"shortcuts/calendar/",
21+
"shortcuts/doc/",
2122
"shortcuts/drive/",
23+
"shortcuts/im/",
2224
"shortcuts/mail/",
25+
"shortcuts/markdown/",
2326
"shortcuts/okr/",
27+
"shortcuts/sheets/",
28+
"shortcuts/slides/",
2429
"shortcuts/task/",
2530
"shortcuts/whiteboard/",
26-
"shortcuts/im/",
31+
"shortcuts/wiki/",
2732
}
2833

2934
// legacyOutputImportPath is the import path of the package that declares the

lint/errscontract/rules_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,11 +944,18 @@ func TestCheckNoLegacyCommonHelperCall_RejectsLegacyHelpersOnMigratedPath(t *tes
944944
"HandleApiResult",
945945
}
946946
paths := []string{
947+
"shortcuts/calendar/calendar_create.go",
948+
"shortcuts/doc/docs_fetch_v2.go",
947949
"shortcuts/drive/drive_search.go",
950+
"shortcuts/im/im_messages_send.go",
948951
"shortcuts/mail/mail_send.go",
952+
"shortcuts/markdown/markdown_fetch.go",
949953
"shortcuts/okr/okr_progress_create.go",
954+
"shortcuts/sheets/helpers.go",
955+
"shortcuts/slides/slides_create.go",
950956
"shortcuts/task/task_update.go",
951957
"shortcuts/whiteboard/whiteboard_query.go",
958+
"shortcuts/wiki/wiki_node_get.go",
952959
}
953960
for _, path := range paths {
954961
for _, helper := range helpers {
@@ -997,6 +1004,34 @@ func boom() {
9971004
}
9981005
}
9991006

1007+
func TestCheckNoLegacyCommonHelperCall_CoversCCMPathsWithAliasAndFunctionValue(t *testing.T) {
1008+
paths := []string{
1009+
"shortcuts/doc/docs_fetch_v2.go",
1010+
"shortcuts/markdown/markdown_fetch.go",
1011+
"shortcuts/sheets/helpers.go",
1012+
"shortcuts/slides/slides_create.go",
1013+
"shortcuts/wiki/wiki_node_get.go",
1014+
}
1015+
src := `package migrated
1016+
1017+
import c "github.com/larksuite/cli/shortcuts/common"
1018+
1019+
func boom() {
1020+
f := c.FlagErrorf
1021+
_ = f
1022+
c.WrapInputStatError(nil)
1023+
}
1024+
`
1025+
for _, path := range paths {
1026+
t.Run(path, func(t *testing.T) {
1027+
v := CheckNoLegacyCommonHelperCall(path, src)
1028+
if len(v) != 2 {
1029+
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on %s, got %d: %+v", path, len(v), v)
1030+
}
1031+
})
1032+
}
1033+
}
1034+
10001035
func TestCheckNoLegacyCommonHelperCall_AllowsNonMigratedPath(t *testing.T) {
10011036
src := `package contact
10021037

shortcuts/doc/clipboard.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"regexp"
1212
"runtime"
1313
"strings"
14+
15+
"github.com/larksuite/cli/errs"
1416
)
1517

1618
// readClipboardImageBytes reads the current clipboard image and returns the
@@ -35,13 +37,13 @@ func readClipboardImageBytes() ([]byte, error) {
3537
case "linux":
3638
data, err = readClipboardLinux()
3739
default:
38-
return nil, fmt.Errorf("clipboard image upload is not supported on %s", runtime.GOOS)
40+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image upload is not supported on %s", runtime.GOOS)
3941
}
4042
if err != nil {
4143
return nil, err
4244
}
4345
if len(data) == 0 {
44-
return nil, fmt.Errorf("clipboard contains no image data")
46+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")
4547
}
4648
return data, nil
4749
}
@@ -91,9 +93,9 @@ func readClipboardDarwin() ([]byte, error) {
9193
}
9294

9395
if stderrText != "" {
94-
return nil, fmt.Errorf("clipboard contains no image data (osascript: %s)", stderrText)
96+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (osascript: %s)", stderrText)
9597
}
96-
return nil, fmt.Errorf("clipboard contains no image data")
98+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data")
9799
}
98100

99101
// runOsascript invokes osascript with a single AppleScript expression and
@@ -188,14 +190,14 @@ func decodeOsascriptData(s string) ([]byte, error) {
188190
// decodeHex decodes an uppercase hex string (as produced by osascript) to bytes.
189191
func decodeHex(h string) ([]byte, error) {
190192
if len(h)%2 != 0 {
191-
return nil, fmt.Errorf("odd hex length")
193+
return nil, fmt.Errorf("odd hex length") //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
192194
}
193195
b := make([]byte, len(h)/2)
194196
for i := 0; i < len(h); i += 2 {
195197
hi := hexVal(h[i])
196198
lo := hexVal(h[i+1])
197199
if hi < 0 || lo < 0 {
198-
return nil, fmt.Errorf("invalid hex char at %d", i)
200+
return nil, fmt.Errorf("invalid hex char at %d", i) //nolint:forbidigo // intermediate decode helper; result discarded by caller on error
199201
}
200202
b[i/2] = byte(hi<<4 | lo)
201203
}
@@ -237,12 +239,12 @@ $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
237239
if msg == "" {
238240
msg = err.Error()
239241
}
240-
return nil, fmt.Errorf("clipboard read failed (%s)", msg)
242+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard read failed (%s)", msg)
241243
}
242244
b64 := strings.TrimSpace(string(out))
243245
data, decErr := base64.StdEncoding.DecodeString(b64)
244246
if decErr != nil {
245-
return nil, fmt.Errorf("clipboard image decode failed: %w", decErr)
247+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image decode failed: %s", decErr).WithCause(decErr)
246248
}
247249
return data, nil
248250
}
@@ -325,15 +327,15 @@ func readClipboardLinux() ([]byte, error) {
325327
foundTool = true
326328
out, err := exec.Command(t.name, t.args...).Output()
327329
if err != nil {
328-
lastErr = fmt.Errorf("clipboard image read failed via %s: %w", t.name, err)
330+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard image read failed via %s: %s", t.name, err).WithCause(err)
329331
continue
330332
}
331333
if len(out) == 0 {
332-
lastErr = fmt.Errorf("clipboard contains no image data (%s returned empty output)", t.name)
334+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no image data (%s returned empty output)", t.name)
333335
continue
334336
}
335337
if t.validatePNG && !hasPNGMagic(out) {
336-
lastErr = fmt.Errorf("clipboard contains no PNG image data (%s output is not a PNG)", t.name)
338+
lastErr = errs.NewValidationError(errs.SubtypeFailedPrecondition, "clipboard contains no PNG image data (%s output is not a PNG)", t.name)
337339
continue
338340
}
339341
return out, nil
@@ -342,8 +344,8 @@ func readClipboardLinux() ([]byte, error) {
342344
if foundTool && lastErr != nil {
343345
return nil, lastErr
344346
}
345-
return nil, fmt.Errorf(
346-
"clipboard image read failed: no supported tool found. " +
347-
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager " +
347+
return nil, errs.NewValidationError(errs.SubtypeFailedPrecondition,
348+
"clipboard image read failed: no supported tool found. "+
349+
"Install one of xclip, wl-clipboard, or xsel via your distro's package manager "+
348350
"(apt, dnf, pacman, apk, brew, etc.).")
349351
}

shortcuts/doc/doc_errors.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package doc
5+
6+
import "github.com/larksuite/cli/errs"
7+
8+
// wrapDocNetworkErr returns err unchanged when it is already a typed errs.*
9+
// error (preserving its subtype / code / log_id from the runtime boundary),
10+
// and only wraps a raw, unclassified error as a transport-level network error.
11+
func wrapDocNetworkErr(err error, format string, args ...any) error {
12+
if _, ok := errs.ProblemOf(err); ok {
13+
return err
14+
}
15+
return errs.NewNetworkError(errs.SubtypeNetworkTransport, format, args...).WithCause(err)
16+
}

shortcuts/doc/doc_media_download.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010

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

13+
"github.com/larksuite/cli/errs"
1314
"github.com/larksuite/cli/extension/fileio"
14-
"github.com/larksuite/cli/internal/output"
1515
"github.com/larksuite/cli/internal/validate"
1616
"github.com/larksuite/cli/shortcuts/common"
1717
)
@@ -51,10 +51,10 @@ var DocMediaDownload = common.Shortcut{
5151
overwrite := runtime.Bool("overwrite")
5252

5353
if err := validate.ResourceName(token, "--token"); err != nil {
54-
return output.ErrValidation("%s", err)
54+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err).WithParam("--token")
5555
}
5656
if _, err := runtime.ResolveSavePath(outputPath); err != nil {
57-
return output.ErrValidation("unsafe output path: %s", err)
57+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
5858
}
5959

6060
fmt.Fprintf(runtime.IO().ErrOut, "Downloading: %s %s\n", mediaType, common.MaskToken(token))
@@ -73,7 +73,7 @@ var DocMediaDownload = common.Shortcut{
7373
ApiPath: apiPath,
7474
})
7575
if err != nil {
76-
return output.ErrNetwork("download failed: %v", err)
76+
return wrapDocNetworkErr(err, "download failed: %v", err)
7777
}
7878
defer resp.Body.Close()
7979

@@ -86,14 +86,14 @@ var DocMediaDownload = common.Shortcut{
8686
// Validate final path after extension append
8787
if finalPath != outputPath {
8888
if _, err := runtime.ResolveSavePath(finalPath); err != nil {
89-
return output.ErrValidation("unsafe output path: %s", err)
89+
return errs.NewValidationError(errs.SubtypeInvalidArgument, "unsafe output path: %s", err).WithParam("--output").WithCause(err)
9090
}
9191
}
9292

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

@@ -102,7 +102,7 @@ var DocMediaDownload = common.Shortcut{
102102
ContentLength: resp.ContentLength,
103103
}, resp.Body)
104104
if err != nil {
105-
return common.WrapSaveErrorByCategory(err, "io")
105+
return common.WrapSaveErrorTyped(err)
106106
}
107107

108108
savedPath, _ := runtime.ResolveSavePath(finalPath)

0 commit comments

Comments
 (0)