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
191 lines
6.7 KiB
Go
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
|
|
}
|