Files
wehub-resource-sync bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

191 lines
6.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package errscontract
import (
"go/ast"
"go/types"
"path/filepath"
"strings"
"golang.org/x/tools/go/packages"
)
// errsPkgPath is the canonical import path of the typed-errors package.
// CheckDeclaredSubtype's typed-resolution pass verifies that a `pkg.SubtypeXxx` selector's
// resolved object belongs to this exact package — selector-name matching
// alone would have falsely accepted an identically-named constant from a
// foreign package.
const errsPkgPath = "github.com/larksuite/cli/errs"
// TypedScope captures the workspace-wide type information used by CheckDeclaredSubtype's
// typed-resolution pass. The zero value is a no-op (typed pass disabled);
// LoadTypedScope populates it.
//
// Once populated:
// - typedFiles maps an absolute Go file path to the *types.Info of its
// package. The walker uses it to resolve selector / ident references on
// a per-file basis: Info.Uses[ident] yields the *types.Object pointed
// at by that identifier, including the originating package.
// - errsSubtypeConsts holds the typed Subtype constants declared in the
// errs package. A resolved object is a "real" Subtype only when it
// appears in this set.
type TypedScope struct {
typedFiles map[string]*types.Info
errsSubtypeConsts map[string]*types.Const
}
// Enabled reports whether the typed-resolution pass can answer questions
// about errs.Subtype references. It requires both:
//
// - typedFiles non-empty (go/packages.Load produced usable type info);
// - errsSubtypeConsts non-empty (the canonical errs.Subtype const set
// was actually discovered).
//
// Requiring both avoids the half-loaded failure mode where typed-file
// indexing succeeded but the errs package was not visited — every
// resolution attempt would then claim "foreign const" and over-reject.
// Callers fall back to AST-only resolution when Enabled returns false.
func (s *TypedScope) Enabled() bool {
if s == nil {
return false
}
return len(s.typedFiles) > 0 && len(s.errsSubtypeConsts) > 0
}
// LookupFileInfo returns the per-package types.Info covering the given Go
// file (path matching the absolute path used during the load). Callers use
// it to resolve *ast.Ident → *types.Object via Info.Uses.
func (s *TypedScope) LookupFileInfo(absPath string) (*types.Info, bool) {
if s == nil {
return nil, false
}
info, ok := s.typedFiles[filepath.Clean(absPath)]
return info, ok
}
// LoadTypedScope loads the workspace rooted at root with full type
// information and returns a scope ready for CheckDeclaredSubtype typed resolution. A
// non-nil error reports an unrecoverable failure (the loader could not
// even start); a successful return with Enabled() == false indicates the
// loader ran but produced no usable type info (e.g. the errs package was
// missing) — in which case the caller should fall back silently to the
// AST-only path.
func LoadTypedScope(root string) (*TypedScope, error) {
cfg := &packages.Config{
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedCompiledGoFiles |
packages.NeedImports |
packages.NeedDeps |
packages.NeedTypes |
packages.NeedSyntax |
packages.NeedTypesInfo,
Dir: root,
Tests: false,
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
return nil, err
}
scope := &TypedScope{
typedFiles: map[string]*types.Info{},
errsSubtypeConsts: map[string]*types.Const{},
}
packages.Visit(pkgs, nil, func(p *packages.Package) {
if p == nil || p.TypesInfo == nil {
return
}
// Index file → TypesInfo for the walker.
for _, f := range p.CompiledGoFiles {
scope.typedFiles[filepath.Clean(f)] = p.TypesInfo
}
// Capture declared Subtype constants from the canonical errs package
// so CheckDeclaredSubtype can reject selectors that resolve to a foreign-package
// const sharing the same name.
if p.PkgPath == errsPkgPath && p.Types != nil {
collectSubtypeConsts(p.Types, scope.errsSubtypeConsts)
}
})
return scope, nil
}
// collectSubtypeConsts scans a *types.Package for exported constants of
// type errs.Subtype whose name starts with "Subtype" and records them by
// name. The "Subtype" name prefix is enforced so the helper aligns with
// the CheckDeclaredSubtype AST pass and avoids matching the underlying `Subtype` type
// definition itself.
func collectSubtypeConsts(pkg *types.Package, into map[string]*types.Const) {
if pkg == nil || pkg.Scope() == nil {
return
}
for _, name := range pkg.Scope().Names() {
if !strings.HasPrefix(name, "Subtype") || name == "Subtype" {
continue
}
obj := pkg.Scope().Lookup(name)
c, ok := obj.(*types.Const)
if !ok {
continue
}
// Verify the constant's type is errs.Subtype (not e.g. a foreign
// "Subtype"-named string alias re-exported from this package).
named, ok := c.Type().(*types.Named)
if !ok {
continue
}
if named.Obj() == nil || named.Obj().Name() != "Subtype" ||
named.Obj().Pkg() == nil || named.Obj().Pkg().Path() != errsPkgPath {
continue
}
into[name] = c
}
}
// ResolveSubtypeIdent inspects the identifier used as the value of a
// `Subtype:` composite-literal field and reports the typed-scope verdict
// via the (resolved, ok) tuple:
//
// - (true, true): the identifier is a declared errs.Subtype constant.
// The AST pass may skip its nameset check for this site.
// - (false, true): definitive rejection — the identifier resolved to a
// constant in a non-errs package, or to a non-Subtype constant inside
// errs. Caller MUST NOT fall back to AST resolution; CheckDeclaredSubtype should
// reject this site.
// - (false, false): typed scope cannot decide (scope disabled, no file
// info, sel==nil, no type info for the identifier, or the resolved
// object is not a constant). Caller defers to AST-only resolution.
func (s *TypedScope) ResolveSubtypeIdent(absPath string, sel *ast.Ident) (resolved, ok bool) {
if !s.Enabled() {
return false, false
}
info, found := s.LookupFileInfo(absPath)
if !found || info == nil || sel == nil {
return false, false
}
obj, found := info.Uses[sel]
if !found || obj == nil {
// No type info for this identifier — caller falls back to AST.
return false, false
}
c, isConst := obj.(*types.Const)
if !isConst {
return false, false
}
if c.Pkg() == nil || c.Pkg().Path() != errsPkgPath {
// Foreign-package constant assigned to a Subtype: slot. Reject —
// the caller routes ALL selectors through this path regardless of
// name shape, so this branch fires for both `foreign.SubtypeFoo`
// and `foreign.MyKind`.
return false, true
}
if _, declared := s.errsSubtypeConsts[c.Name()]; !declared {
// In the errs package but not a Subtype const (defense-in-depth).
return false, true
}
return true, true
}