semantic

package
v1.3.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package semantic — @default literal validation: type/element support, primitive-kind map, helpers.

Cross-decorator combination rules (defaults, bindings, single-binding, passthrough body) + ref walking helpers.

Symbol-table population + cross-file package-name check + extend-service merge.

Decorator duplicate / scope / conflict / sensitive checks.

Diagnostic code constants + helper builders.

Decorator placement checks (decl, field, scope).

Resolved field IR (M3, step 6): the single, LAYER-AGNOSTIC view of a field's resolved facts — what the field MEANS in the DSL (its category, underlying primitive, home package, nilability), independent of how Go renders it. The LSP and the semantic checks read these directly; codegen derives the Go-specific bits (the *T pointer wrap, the json tag, the Go type string) from them. Computing each fact ONCE here is what stops the recurring "semantic resolves a scalar one way, codegen another" drift (e.g. the cross-package-promoted scalar nilability gap).

Package semantic performs whole-package validation on parsed ast.File values and produces a merged, name-indexed Package for downstream tools.

Responsibilities:

  • Package-name consistency across files.
  • Symbol tables for types, enums, errors, scalars, middlewares.
  • Primary / `extend service` merge.
  • Duplicate names (top-level, fields, methods, routes) and uniform enum value kinds.
  • Decorator placement, arity, argument literal types, value-set enums, cross-references (errors / middlewares / security schemes / requiresOneOf field idents), and value-range checks.
  • Field-type compatibility for validator decorators (string validators only on strings, etc.).
  • Mixin field expansion: cycle, conflict, and generic-arity detection.
  • Generic instantiation: arg arity, non-generic-with-args, and type-parameter scoping.

Single-package Analyze uses a folder-merge import model and rejects qualified names; AnalyzeProject resolves cross-package qualified refs against the project's package set. Diagnostics carry stable lexer.Diagnostic.Code identifiers (`decorator/arity`, `mixin/conflict`, `generic/arity`, …) so the LSP and docs site can reference each rule individually.

Service-method shape checks (uniqueness, route collisions) + PathString helper.

Index

Constants

View Source
const (
	// CodeDecoratorUnknown fires when `@name` is not in the registry.
	// Decorators are a closed set by design (no escape-hatch).
	CodeDecoratorUnknown = "decorator/unknown"
	// CodeDecoratorPlacement fires when a known decorator appears at a
	// site outside its declared [Spec.Levels].
	CodeDecoratorPlacement = "decorator/placement"
	// CodeDecoratorDuplicate fires when the same `@name` appears twice
	// in the same scope. Args do not disambiguate.
	CodeDecoratorDuplicate = "decorator/duplicate"
	// CodeDecoratorArity fires when the count of arguments to `@name`
	// is below ArgMin or above ArgMax.
	CodeDecoratorArity = "decorator/arity"
	// CodeDecoratorArgType fires when an argument literal kind does
	// not match the expected ArgKind for the position.
	CodeDecoratorArgType = "decorator/argtype"
	// CodeDecoratorArgValue fires when an argument value falls outside
	// the allowed enum set (e.g. `@format(garbage)`).
	CodeDecoratorArgValue = "decorator/argvalue"
	// CodeDecoratorRange fires when a numeric pair is out of order
	// (e.g. `@length(20, 5)`) or violates a per-decorator bound.
	CodeDecoratorRange = "decorator/range"
	// CodeDecoratorTypeMismatch fires when a validator decorator is
	// applied to an incompatible field/scalar primitive (e.g.
	// `@length` on `int`).
	CodeDecoratorTypeMismatch = "decorator/typemismatch"
	// CodeDecoratorRef fires when a decorator argument names an entity
	// (error / middleware / field / security scheme) that does not
	// exist in scope.
	CodeDecoratorRef = "decorator/ref"
	// CodeDecoratorRedundant fires when two decorators say the same
	// thing redundantly (warning, not error). Example: `@nullable`
	// on a `T?` field.
	CodeDecoratorRedundant = "decorator/redundant"
	// CodeDecoratorConflict fires when two decorators on the same site
	// have semantics that contradict. Example: `@sensitive` paired with
	// a wire-shaping validator like `@length` (sensitive fields never
	// cross the wire so wire-level constraints are meaningless).
	CodeDecoratorConflict = "decorator/conflict"
	// CodeDefaultNeedsOptional fires (severity warning) when `@default` is
	// placed on a non-optional, non-`@path` field. The default fires when the
	// value is absent, so the field is conceptually optional; `craftgo fmt`
	// adds the `?` on save, after which types.go, validate.go, and the OpenAPI
	// agree (optional + nullable). Until then the artifacts can disagree.
	CodeDefaultNeedsOptional = "decorator/default-needs-optional"
	// CodeFlagEmptyParens fires (severity warning) when a Flag
	// decorator (one that never takes arguments) is written with empty
	// parens — `@positive()` instead of `@positive`. Warning only:
	// `craftgo fmt` strips the parens on save so canonical form is
	// parens-free.
	CodeFlagEmptyParens = "decorator/flag-empty-parens"
	// CodeArgPreferIdent fires (severity warning) when a decorator
	// argument names a registered identifier (format name, security
	// scheme, ...) but the source spells it as a quoted string. The
	// canonical form is bare ident — `@format(email)` not
	// `@format("email")`. `craftgo fmt` rewrites on save.
	CodeArgPreferIdent = "decorator/arg-prefer-ident"
	// CodeBoundOverflow fires when a numeric bound literal exceeds
	// the field type's capacity. `int8 @lte(300)` — 300 overflows
	// int8 (max 127). Without this check codegen emits an untyped
	// integer literal that fails to compile against the typed field.
	CodeBoundOverflow = "decorator/bound-overflow"
	// CodeBoundEmptyRange fires when two comparison decorators on the
	// same field define an empty value set. `@gt(5) @lt(5)`,
	// `@gte(N) @lt(N)`, and `@gt(N) @lte(N)` all reject every value.
	CodeBoundEmptyRange = "decorator/empty-range"
	// CodeMutExSingleField fires when `@mutuallyExclusive` is given
	// fewer than 2 fields. The runtime check (`n > 1`) is provably
	// unreachable.
	CodeMutExSingleField = "decorator/single-field-mutex"
	// CodeDuplicateGroupField fires when a cross-field validator
	// (`@requiresOneOf`, `@mutuallyExclusive`) lists the same field
	// name twice. Codegen would emit `v.A == nil && v.A == nil` which
	// `go vet` rejects as a redundant boolean expression.
	CodeDuplicateGroupField = "decorator/duplicate-group-field"
	// CodeCrossFieldNotOptional fires when a cross-field validator
	// (`@requiresOneOf`, `@mutuallyExclusive`) references a field that
	// is neither optional (`?`) nor `@nullable`. Presence is then
	// ambiguous: OpenAPI expresses the group with key-presence
	// (`required` / `not.required`) while the runtime validator falls
	// back to zero-value emptiness (`== ""` / `== 0`), so the spec and
	// the server disagree on whether an empty-but-present value counts.
	// Requiring pointer-backed fields makes "present" mean the same
	// thing on both sides.
	CodeCrossFieldNotOptional = "decorator/cross-field-not-optional"
	// CodeMapKeyType fires when a map key type is not a usable, JSON-
	// serialisable map key. A generic type-parameter or a non-comparable
	// type fails to compile; a bool / float / struct key compiles but
	// json.Marshal cannot serialise it (JSON object keys are strings). Only
	// a string / int* / uint* key, or a scalar / enum over one, is allowed.
	CodeMapKeyType = "type/map-key"
	// CodeDuplicatePathVar fires when a route template repeats a path
	// variable name (`/items/{id}/x/{id}`). net/http's ServeMux panics on
	// a duplicate wildcard at registration.
	CodeDuplicatePathVar = "route/duplicate-path-var"
	// CodeDuplicateWireName fires when two request fields bind to the same
	// wire parameter name on the same source (`a @query("x")  b
	// @query("x")`). The OpenAPI emits a duplicate (name, in) parameter
	// (an invalid spec) and the binder reads the same value into both.
	CodeDuplicateWireName = "binding/duplicate-wire-name"

	// CodePackageMismatch fires when files disagree on the `package`
	// name.
	CodePackageMismatch = "decl/package-mismatch"
	// CodeDuplicateDecl fires when two top-level declarations share a
	// name across the merged package.
	CodeDuplicateDecl = "decl/duplicate"
	// CodeDeclBuiltinName fires when a type / enum / scalar / error is
	// named after a built-in type spelling (`int`, `string`, `any`, ...).
	// The generated Go type would shadow the built-in and fail to compile.
	CodeDeclBuiltinName = "decl/builtin-name"
	// CodeDeclNameCase fires (severity warning) when a top-level decl
	// - type / error / enum / service / middleware / scalar - does
	// not start with an uppercase letter. Lower-case decl names are
	// emitted verbatim by codegen, producing UNEXPORTED Go types
	// that cannot be imported across packages.
	CodeDeclNameCase = "decl/name-case"
	// CodeFieldNameCollision fires (severity warning) when two field
	// names in the same type / error body normalise to the same Go
	// identifier under [internal/idents.GoFieldName] (e.g. `user_id`
	// and `userId` both → `UserID`). Codegen still emits the struct
	// using `_2`, `_3`, ... suffixes so the result compiles, but
	// the JSON wire shape carries both DSL names verbatim - a quiet
	// schema duplication the user almost certainly did not intend.
	CodeFieldNameCollision = "field/name-collision"
	// CodeEnumValueCollision fires (severity warning) when two enum
	// values in the same enum normalise to the same Go const name
	// (e.g. `created` and `Created` both → `<Enum>Created`).
	// Codegen emits the trailing duplicates with `_2`, `_3`, ...
	// suffixes so the package compiles, but the wire payload
	// (string or int) of both values stays distinct - a quiet
	// duplication the user usually did not intend.
	CodeEnumValueCollision = "enum/value-collision"
	// CodeDeclGoNameCollision fires (severity ERROR) when two
	// top-level decls in the same package produce the same Go
	// identifier under codegen's name-mangling rules. Examples
	// caught by this rule:
	//
	//   - `type FooErr` + `error Foo` - both emit `type FooErr`
	//   - `type FooBody` + `error Foo { ... }` - both emit `type FooBody`
	//   - `type FooMiddleware` + `middleware Foo` - same
	//
	// Auto-suffixing decls would silently rename a symbol the user
	// references in their own Go code, so this is a hard error
	// rather than the soft warning used for FIELD-level dedup.
	CodeDeclGoNameCollision = "decl/go-name-collision"

	// CodeDuplicateField fires when two fields in the same type / error
	// body share a name.
	CodeDuplicateField = "field/duplicate"

	// CodeInvalidGoName fires when a field name maps to an invalid Go
	// identifier — empty (e.g. `_`, `__`) or digit-leading (e.g. `_2`,
	// which normalises to `2`). Codegen would emit uncompilable / unexported
	// Go, so reject at design time with a clean message instead.
	CodeInvalidGoName = "field/invalid-go-name"

	// CodeEnumDuplicateName fires for two enum values with the same
	// identifier.
	CodeEnumDuplicateName = "enum/duplicate-name"
	// CodeEnumMixedTypes fires when an enum mixes bare / int / string
	// values.
	CodeEnumMixedTypes = "enum/mixed-types"
	// CodeEnumDuplicateLiteral fires when two enum values share an
	// int or string literal.
	CodeEnumDuplicateLiteral = "enum/duplicate-literal"
	// CodeEnumEmpty fires when an `enum X { }` has zero values. An
	// empty enum has no value that passes validation and emits
	// `enum: []` which violates JSON Schema 2020-12.
	CodeEnumEmpty = "enum/empty-values"

	// CodeServiceDuplicate fires for two primary `service` decls of
	// the same name.
	CodeServiceDuplicate = "service/duplicate"
	// CodeServiceExtendOrphan fires when an `extend service` has no
	// primary declaration in the package.
	CodeServiceExtendOrphan = "service/extend-orphan"
	// CodeExtendDecoratorNotMethod fires when an `extend service` block
	// carries a decorator that has no method-level form (e.g. `@prefix`).
	// Such decorators must sit on the primary service declaration;
	// putting them on an extend block would propagate to every method
	// in the block, which is meaningless for service-only directives.
	CodeExtendDecoratorNotMethod = "service/extend-decorator-not-method"
	// CodeServiceDuplicateMethod fires for two methods sharing a name
	// inside one service (after extends merge).
	CodeServiceDuplicateMethod = "service/duplicate-method"
	// CodeServiceDuplicateRoute fires for two methods sharing the
	// same VERB+path tuple (after extends merge).
	CodeServiceDuplicateRoute = "service/duplicate-route"

	// CodeBindingConflict fires when a field has more than one of
	// `@path / @query / @header / @cookie / @body / @form`.
	CodeBindingConflict = "binding/conflict"
	// CodeBindingType fires when `@path`, `@header`, or `@cookie` is
	// applied to a field whose type is not a non-array, non-optional
	// `string`. The wire formats those decorators target carry only
	// strings (URL segments, header values, cookie values), and the
	// codegen would otherwise silently skip the field at gen time -
	// surfacing the mismatch at design time gives the author an
	// actionable error.
	CodeBindingType = "binding/type"
	// CodeBindingVerb fires when `@body` or `@form` sits on a request
	// field of a non-body verb (GET / HEAD / DELETE / OPTIONS). Those
	// handlers decode no request body, so the field would be silently
	// dropped at gen time — surfacing it at design time prevents the
	// silent data loss.
	CodeBindingVerb = "binding/verb"
	// CodeFilePosition fires when a `file` field appears where the
	// multipart binder cannot reach it: inside a response type, or nested
	// below the top level of a request body. The form-binding codegen scans
	// only the resolved top-level request fields, so a `file` elsewhere is
	// silently emitted as a JSON-encoded `*multipart.FileHeader` the server
	// can never populate. `file` is valid only as a top-level request field
	// (directly or carried in via a mixin).
	CodeFilePosition = "binding/file-position"
	// CodeServiceCollision fires when two packages in the same
	// project both declare a primary `service` of the same name.
	// The generated codegen layout keys output directories by
	// service name (`internal/routes/<svc>/`, `internal/handler/<svc>/`),
	// so a collision would silently overwrite one package's
	// scaffolds with the other's. Surface every conflicting
	// declaration so the author can rename one.
	CodeServiceCollision = "service/collision"
	// CodeMiddlewareCollision fires when two packages in the same
	// project both declare a `middleware` of the same name. Cross-
	// package middleware references are global by design, so a
	// collision would make `@middlewares(Name)` ambiguous - the
	// resolver picks the first match silently. The diagnostic
	// surfaces every conflicting declaration so the author can
	// rename or consolidate.
	CodeMiddlewareCollision = "middleware/collision"
	// CodePassthroughBody fires when a method tagged `@passthrough`
	// declares a `request` or `response` block. Passthrough endpoints
	// hand the raw `http.ResponseWriter` and `*http.Request` to logic;
	// any framework-side request/response shape would be ignored, so
	// the analyser rejects the mistake up front.
	CodePassthroughBody = "passthrough/has-body"

	// CodeQualifiedRef fires for a `pkg.Type` reference. The current
	// resolver uses folder-merge imports and rejects qualified names.
	CodeQualifiedRef = "ref/qualified"

	// CodeMixinUnresolved fires when a mixin reference does not name
	// a type declared in the package.
	CodeMixinUnresolved = "mixin/unresolved"
	// CodeMixinNonType fires when a mixin reference resolves to a
	// non-type entity (enum, error, scalar, middleware).
	CodeMixinNonType = "mixin/non-type"
	// CodeMixinCycle fires when expanding a mixin would loop back
	// onto a type already on the expansion stack.
	CodeMixinCycle = "mixin/cycle"
	// CodeMixinConflict fires when expansion produces two fields
	// with the same name (mixin vs host or mixin vs mixin).
	CodeMixinConflict = "mixin/conflict"
	// CodeMixinArity fires when a generic mixin's argument count
	// disagrees with the target's TypeParams count.
	CodeMixinArity = "mixin/arity"

	// CodeGenericArity fires when a generic instance's argument count
	// disagrees with the target decl's TypeParams.
	CodeGenericArity = "generic/arity"
	// CodeGenericNonGeneric fires when a non-generic type is referenced
	// with `<...>` arguments.
	CodeGenericNonGeneric = "generic/non-generic"
	// CodeGenericOptionalArg fires when a generic type argument carries a
	// trailing `?` (`Page<Item?>`). The optionality has no single, well-
	// defined position once the argument is substituted into the decl's
	// body, so the Go type and the OpenAPI schema disagree about whether it
	// applies to the element or the surrounding collection. Declare the
	// nullability on a concrete field of the generic instead.
	CodeGenericOptionalArg = "generic/optional-arg"

	// CodePathBaseFormat warns when [Options.BasePath] is malformed -
	// missing leading slash, trailing slash, or contains `//`. Code-
	// gen normalises these so this is a warning, not an error.
	CodePathBaseFormat = "path/base-format"
	// CodePathCollision fires when two methods (across any service)
	// resolve to the same VERB + final-path tuple.
	CodePathCollision = "path/collision"
	// CodeDuplicateOperation fires when two methods resolve to the same
	// OpenAPI operationId — auto-prefixing removes same-method-name
	// collisions, so a survivor comes from an explicit `@operationId(...)`
	// that two methods share (or that equals another method's auto id),
	// which would emit an invalid spec.
	CodeDuplicateOperation = "operation/duplicate-id"
	// CodePathParamMissing fires when a `{name}` segment in the
	// resolved route has no corresponding field binding in the
	// method's request type.
	CodePathParamMissing = "path/param-missing"
	// CodePathParamOrphan fires when a request field uses `@path` /
	// `@path("name")` but the resolved route has no matching
	// `{name}` segment.
	CodePathParamOrphan = "path/param-orphan"
	// CodePathHealthConflict fires when a user-declared method's
	// resolved route equals one of the runtime-reserved health paths
	// (`/healthz`, `/readyz` by default).
	CodePathHealthConflict = "path/health-conflict"

	// CodeImportUnresolved fires when `import "path"` does not
	// correspond to a folder under the design root.
	CodeImportUnresolved = "import/unresolved"
	// CodeImportEscape fires when an import path uses `..` or starts
	// with `/` to escape the design root.
	CodeImportEscape = "import/escape"
	// CodeImportDuplicate fires when one file imports the same path
	// twice (with or without matching aliases) - a clear redundancy
	// the parser cannot detect without per-file context.
	CodeImportDuplicate = "import/duplicate"
	// CodeImportAliasConflict fires when two imports in the same
	// file resolve to the same alias (explicit or implicit), making
	// later qualified references like `alias.Type` ambiguous.
	CodeImportAliasConflict = "import/alias-conflict"
	// CodeImportSelf fires when a file imports a folder whose files
	// share its own `package X` declaration - the import is a no-op
	// since the analyser already merges them by package name.
	CodeImportSelf = "import/self"
	// CodeRefUnknownPackage fires when `pkg.Type` references a
	// package whose `package X` declaration doesn't appear anywhere
	// in the project.
	CodeRefUnknownPackage = "ref/unknown-package"
	// CodeRefUnknownSymbol fires when the package resolves correctly
	// but doesn't declare the named type.
	CodeRefUnknownSymbol = "ref/unknown-symbol"
	// CodeScalarBadPrimitive fires when a `scalar Name Primitive`
	// declaration uses a non-builtin word in the primitive slot
	// (e.g. another type name, a typo, or the scalar's own name).
	// The scalar's underlying type must be a primitive the framework
	// knows how to validate; user-defined types in this slot would
	// silently break inheritance and produce invalid Go.
	CodeScalarBadPrimitive = "scalar/bad-primitive"
)

Variables

View Source
var Registry = map[string]Spec{

	"doc": {
		Name:   "doc",
		Levels: LvlFile | LvlType | LvlField | LvlService | LvlMethod | LvlEnum | LvlEnumValue | LvlError | LvlScalar | LvlMiddleware | LvlErrorField,
		Doc:    "Free-form documentation surfaced in OpenAPI and IDE hover.",
		Args:   ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}},
	},
	"deprecated": {
		Name:   "deprecated",
		Levels: LvlFile | LvlType | LvlField | LvlService | LvlMethod | LvlEnumValue | LvlMiddleware | LvlErrorField,
		Doc:    "Marks the construct as deprecated; OpenAPI emits the deprecated flag.",
		Args:   ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}},
	},
	"example": {
		Name:   "example",
		Levels: LvlField | LvlErrorField,
		Doc:    "Example value rendered in the OpenAPI schema for this field. Argument is a literal (string / int / float / bool / null) or an array of those. Object examples are not accepted — a struct example is composed from each field's own @example; document a free-form any/map field's shape with @doc.",
		Args:   ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgAny}},
	},

	"version": {
		Name:   "version",
		Levels: LvlFile,
		Doc:    "OpenAPI document version (overrides craftgo.design.yaml openapi.version).",
		Args:   ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}},
	},

	"requiresOneOf": {
		Name:   "requiresOneOf",
		Levels: LvlType,
		Doc:    "At least one of the listed fields must be present. Args: variadic idents/strings or a single array literal.",
		Args: ArgsRule{
			Min: 1, Max: -1, Variadic: ArgStringOrIdent,
			AllowArrayShortcut: true,
		},
	},
	"mutuallyExclusive": {
		Name:   "mutuallyExclusive",
		Levels: LvlType,
		Doc:    "At most one of the listed fields may be present. Args: variadic idents/strings or a single array literal.",
		Args: ArgsRule{
			Min: 1, Max: -1, Variadic: ArgStringOrIdent,
			AllowArrayShortcut: true,
		},
	},

	"length": {
		Name: "length", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Exact or [min,max] length for strings.",
		Args:      ArgsRule{Min: 1, Max: 2, Kinds: []ArgKind{ArgInt, ArgInt}},
		AppliesTo: PrimString,
	},
	"minLength": {
		Name: "minLength", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Minimum string length.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgInt}},
		AppliesTo: PrimString,
	},
	"maxLength": {
		Name: "maxLength", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Maximum string length.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgInt}},
		AppliesTo: PrimString,
	},
	"pattern": {
		Name: "pattern", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "RE2 regex the value must match.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}},
		AppliesTo: PrimString,
	},
	"format": {
		Name:   "format",
		Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:    "Named format constraint (e.g. email, uuid, datetime).",
		Args: ArgsRule{
			Min: 1, Max: 1,
			Kinds: []ArgKind{ArgStringOrIdent},
			Enum:  formatValues,
		},
		AppliesTo: PrimString,
	},

	"gt": {
		Name: "gt", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Value must be strictly greater than N (x > N).",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgNumber}},
		AppliesTo: PrimNumber,
	},
	"gte": {
		Name: "gte", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Value must be greater than or equal to N (x >= N).",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgNumber}},
		AppliesTo: PrimNumber,
	},
	"lt": {
		Name: "lt", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Value must be strictly less than N (x < N).",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgNumber}},
		AppliesTo: PrimNumber,
	},
	"lte": {
		Name: "lte", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Value must be less than or equal to N (x <= N).",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgNumber}},
		AppliesTo: PrimNumber,
	},
	"range": {
		Name: "range", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Numeric range [min, max] — both bounds inclusive.",
		Args:      ArgsRule{Min: 2, Max: 2, Kinds: []ArgKind{ArgNumber, ArgNumber}},
		AppliesTo: PrimNumber,
	},
	"positive": {Name: "positive", Levels: LvlField | LvlScalar | LvlErrorField, Doc: "Value must be > 0.", AppliesTo: PrimNumber, Flag: true},
	"negative": {Name: "negative", Levels: LvlField | LvlScalar | LvlErrorField, Doc: "Value must be < 0.", AppliesTo: PrimNumber, Flag: true},
	"multipleOf": {
		Name: "multipleOf", Levels: LvlField | LvlScalar | LvlErrorField,
		Doc:       "Value must be a multiple of N.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgNumber}},
		AppliesTo: PrimNumber,
	},

	"minItems": {
		Name: "minItems", Levels: LvlField | LvlErrorField,
		Doc:       "Minimum array / map length.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgInt}},
		AppliesTo: PrimArray,
	},
	"maxItems": {
		Name: "maxItems", Levels: LvlField | LvlErrorField,
		Doc:       "Maximum array / map length.",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgInt}},
		AppliesTo: PrimArray,
	},
	"uniqueItems": {Name: "uniqueItems", Levels: LvlField | LvlErrorField, Doc: "Array elements must be unique.", AppliesTo: PrimArray, Flag: true},

	"maxSize": {
		Name: "maxSize", Levels: LvlField,
		Doc:       "Upload size cap (bytes / KB / MB / GB).",
		Args:      ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgSize}},
		AppliesTo: PrimFile,
	},
	"mimeTypes": {
		Name: "mimeTypes", Levels: LvlField,
		Doc:       "Allowed Content-Type list for uploads. Args: variadic strings or a single array literal.",
		Args:      ArgsRule{Min: 1, Max: -1, Variadic: ArgString, AllowArrayShortcut: true},
		AppliesTo: PrimFile,
	},

	"default": {
		Name: "default", Levels: LvlField | LvlErrorField,
		Doc:  "Default value applied when field absent.",
		Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgAny}},
	},
	"nullable": {Name: "nullable", Levels: LvlField | LvlErrorField, Doc: "Marks the field as accepting an explicit JSON null.", Flag: true},
	"sensitive": {
		Name: "sensitive", Levels: LvlField | LvlErrorField, Flag: true,
		Doc: "Server-only field: tagged `json:\"-\"` so neither the request decoder nor the response encoder touches it, and skipped entirely from OpenAPI. Cannot combine with any wire-shaping decorator: validators (@length / @gt / @gte / @lt / @lte / @range / @pattern / @format / @minItems / @maxItems / @multipleOf / @positive / @negative / @uniqueItems / @requiresOneOf / @mutuallyExclusive), nullability / defaults (@nullable / @default), or any binding (@body / @path / @query / @header / @cookie / @form). The field stays as a Go struct member that server logic populates / reads internally.",
	},

	"path":   {Name: "path", Levels: LvlField, Doc: "Bind from URL path parameter.", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},
	"query":  {Name: "query", Levels: LvlField, Doc: "Bind from URL query string.", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},
	"header": {Name: "header", Levels: LvlField | LvlErrorField, Doc: "Bind from HTTP request header (request fields) or write to response header (error fields).", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},
	"cookie": {Name: "cookie", Levels: LvlField | LvlErrorField, Doc: "Bind from HTTP cookie (request fields) or set a response cookie (error fields).", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},
	"body":   {Name: "body", Levels: LvlField, Doc: "Bind from request body.", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},
	"form":   {Name: "form", Levels: LvlField, Doc: "Bind from multipart form field.", Args: ArgsRule{Min: 0, Max: 1, Kinds: []ArgKind{ArgString}}},

	"prefix": {
		Name: "prefix", Levels: LvlService,
		Doc:  "Path prefix prepended to every method route.",
		Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}},
	},
	"group": {
		Name: "group", Levels: LvlService,
		Doc:  "Nests the service's generated handlers and service stubs under <service>/<group>/ on disk; does not affect the route or OpenAPI path. Accepts a nested path like \"admin/ops\".",
		Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}},
	},
	"middlewares": {
		Name:       "middlewares",
		Levels:     LvlService | LvlMethod,
		Doc:        "Apply named middlewares; method-level appends to service-level chain. Args: variadic idents or a single array literal.",
		Args:       ArgsRule{Min: 1, Max: -1, Variadic: ArgIdent, AllowArrayShortcut: true},
		Repeatable: true,
	},
	"tags": {
		Name:       "tags",
		Levels:     LvlService | LvlMethod,
		Doc:        "OpenAPI tags. Method-level appends to service-level. Args: variadic idents/strings or a single array literal.",
		Args:       ArgsRule{Min: 1, Max: -1, Variadic: ArgStringOrIdent, AllowArrayShortcut: true},
		Repeatable: true,
	},
	"security": {
		Name:       "security",
		Levels:     LvlService | LvlMethod,
		Doc:        "Security scheme requirements (OpenAPI metadata). Args: variadic scheme idents or a single array literal. Within one decorator the schemes AND-combine; multiple `@security(...)` decorators OR-combine.",
		Args:       ArgsRule{Min: 1, Max: -1, Variadic: ArgIdent, AllowArrayShortcut: true},
		Repeatable: true,
	},
	"ignoreMiddleware": {
		Name:   "ignoreMiddleware",
		Levels: LvlMethod,
		Doc:    "Clear the inherited @middlewares chain on this method. Method-level @middlewares(...) then start from empty instead of appending to the service-level chain.",
		Args:   ArgsRule{Min: 0, Max: 0},
	},
	"ignoreSecurity": {
		Name:   "ignoreSecurity",
		Levels: LvlMethod,
		Doc:    "Clear the inherited @security chain on this method. Useful for public endpoints inside an otherwise-authenticated service.",
		Args:   ArgsRule{Min: 0, Max: 0},
	},
	"ignoreTags": {
		Name:   "ignoreTags",
		Levels: LvlMethod,
		Doc:    "Clear the inherited @tags list on this method. Method-level @tags(...) then start from empty.",
		Args:   ArgsRule{Min: 0, Max: 0},
	},

	"summary":     {Name: "summary", Levels: LvlMethod, Doc: "One-line OpenAPI operation summary.", Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}}},
	"operationId": {Name: "operationId", Levels: LvlMethod, Doc: "Override OpenAPI operationId.", Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgString}}},
	"errors":      {Name: "errors", Levels: LvlMethod, Doc: "Declared error responses for OpenAPI. Args: variadic error idents or a single array literal.", Args: ArgsRule{Min: 1, Max: -1, Variadic: ArgIdent, AllowArrayShortcut: true}, Repeatable: true},
	"status":      {Name: "status", Levels: LvlMethod, Doc: "Override default success status code.", Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgInt}}},

	"passthrough": {Name: "passthrough", Levels: LvlMethod, Doc: "Bypass framework parsing - logic receives the raw http.ResponseWriter and *http.Request and writes the response directly.", Flag: true},

	"timeout":     {Name: "timeout", Levels: LvlMethod, Doc: "Cap the handler's execution time. Returns 503 + cancels context when the deadline elapses.", Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgDuration}}},
	"maxBodySize": {Name: "maxBodySize", Levels: LvlMethod, Doc: "Cap the request body size in bytes. Two enforcement points fire: Content-Length pre-check returns 413 immediately when the declared size exceeds the cap, and MaxBytesReader wraps r.Body so reads past the cap surface as a 400 Read error. Multipart parsers also lift their in-memory budget to this value.", Args: ArgsRule{Min: 1, Max: 1, Kinds: []ArgKind{ArgSize}}},
}

Registry is the closed set of decorators the framework recognises. A `@name` not present here is reported as `decorator/unknown` - there is no escape-hatch by design (see README §"Triết lý").

Levels mirror the table in README §"Decorator compatibility matrix"; keep the two in sync. When in doubt the README table wins, because users read it first.

Functions

func Analyze

func Analyze(files []*ast.File) (*Package, []Diagnostic)

Analyze validates the supplied AST files as a single package and returns the merged Package together with every diagnostic found. The Package value is always non-nil even when diagnostics were reported, so callers (codegen, LSP) can do best-effort downstream work.

Equivalent to AnalyzeWith(files, Options{}).

func AnalyzeProject

func AnalyzeProject(files []*ast.File, opts Options) (*Project, []Diagnostic)

AnalyzeProject parses files into packages keyed by their location under [Options.DesignRoot] and validates cross-package qualified references. The returned Project is always non-nil; consumers may inspect partial results even when diagnostics are reported.

When [Options.DesignRoot] is empty, AnalyzeProject delegates to AnalyzeWith and returns a single-package Project under key "". That makes it safe for the LSP to call AnalyzeProject unconditionally without pre-checking layout.

func AnalyzeWith

func AnalyzeWith(files []*ast.File, opts Options) (*Package, []Diagnostic)

AnalyzeWith is the Analyze variant that accepts cross-reference truth sources. CLI / codegen invocations supply the project's `craftgo.design.yaml` data here; the LSP either supplies the same (when it has read the manifest) or leaves it empty for syntax-only validation.

func BindingKind added in v1.2.0

func BindingKind(ds []*ast.Decorator) string

BindingKind returns the binding kind named by the first binding decorator in ds — "path" / "query" / "header" / "cookie" / "body" / "form" — or "" when none is present. It is the single "which decorator binds this field" classifier the analyser's binding checks and codegen's binders both read, so the two layers cannot disagree on where a field rides. (Valid input carries at most one binding decorator per field — the single-binding rule rejects the rest — so first-match is unambiguous.)

func MethodNameCounts added in v1.2.0

func MethodNameCounts(pkg *Package) map[string]int

MethodNameCounts counts how many times each method name appears across every service. A name shared by two services must be service-qualified in the emitted operationId / component names so they stay globally unique.

func NilableScalarPrimitive added in v1.3.0

func NilableScalarPrimitive(prim string) bool

NilableScalarPrimitive reports whether a scalar's underlying primitive lowers to a Go type that already holds nil (so a scalar over it renders without a pointer wrap): the `bytes` slice and the `any` interface. It is the single authority both the resolved IR (ResolveField) and codegen's pointer-wrap decision cite, so the two layers can't disagree on whether a scalar field needs a `*T`.

func OperationBaseName added in v1.2.0

func OperationBaseName(svcName string, m *ast.Method, counts map[string]int) string

OperationBaseName is the collision-free base for a method's component schema names (`<base>ReqBody`, `<base>RespBody`) and its default operationId: bare when the method name is unique project-wide, service-prefixed when shared.

func OperationID added in v1.2.0

func OperationID(m *ast.Method, base string) string

OperationID returns a method's operationId: an explicit, non-empty `@operationId("...")` override when present, otherwise base.

func PathShape

func PathShape(p *ast.Path) string

PathShape is PathString with every {param} replaced by `{}`. Two routes that route to the same HTTP destination have the same shape even when their parameter names differ — e.g. `/u/{id}` and `/u/{userId}` both reduce to `/u/{}`. Collision-detection keys MUST use this rather than PathString, otherwise a parameter rename silently bypasses the duplicate guard and net/http's mux panics at boot when both routes try to register against the same pattern.

func PathString

func PathString(p *ast.Path) string

func RequestFieldBinding added in v1.2.0

func RequestFieldBinding(f *ast.Field, pathNames map[string]bool, bodyVerb bool) (kind string, auto bool)

RequestFieldBinding resolves where a request field rides once method context is applied: its explicit binding decorator if any (or @sensitive), otherwise the auto-binding rule — an un-decorated field auto-binds to "path" when its name matches a `{param}` segment, to "query" on a body-less verb (there is no body to decode into), or stays "body". auto is true only for an auto-promoted path/query field. This is the single place the request auto-binding rule lives, read by both the analyser's binding checks and codegen's request resolver so the two cannot disagree on where a field rides.

func WireName added in v1.2.0

func WireName(f *ast.Field, kind string) string

WireName returns the on-the-wire name for field f under binding kind (path/query/header/cookie/form): the binding decorator's first non-empty string argument, or the field's own name when none is given. It is the one rule shared by the analyser's binding checks and codegen's binders / OpenAPI parameter emit, so the documented parameter name and the name the handler actually reads cannot disagree.

Types

type ArgKind

type ArgKind uint8

ArgKind classifies the literal shape expected at a positional argument slot. The argument-validation pass maps an ast.Expr to one of these kinds and rejects mismatches with CodeDecoratorArgType.

const (
	// ArgAny accepts any expression. Use sparingly - prefer a tighter
	// kind so the IDE can give a useful "expected X" hint.
	ArgAny ArgKind = iota
	// ArgString matches a [ast.StringLit] (regular or raw).
	ArgString
	// ArgInt matches a [ast.IntLit].
	ArgInt
	// ArgNumber matches int OR float.
	ArgNumber
	// ArgBool matches a [ast.BoolLit].
	ArgBool
	// ArgIdent matches a bare identifier ([ast.IdentExpr]).
	ArgIdent
	// ArgDuration matches a [ast.DurationLit] (`5s`, `100ms`, ...).
	ArgDuration
	// ArgSize matches a [ast.SizeLit] (`1MB`, `8KB`, ...).
	ArgSize
	// ArgStringOrIdent accepts either, used by `@tags` where humans
	// commonly write `@tags(users)` and `@tags("user-mgmt")`
	// interchangeably.
	ArgStringOrIdent
)

func (ArgKind) String

func (k ArgKind) String() string

String returns the human label used in `expected X, got Y` messages. Stable across versions - IDE error explainers reference these names.

type ArgsRule

type ArgsRule struct {
	// Min is the minimum number of positional arguments. 0 allows the
	// no-args form (`@deprecated`).
	Min int
	// Max is the maximum number of positional arguments; -1 means
	// unbounded (variadic).
	Max int
	// Kinds is the per-position expected kind. When the actual arg
	// count exceeds len(Kinds), [Variadic] applies to the remainder.
	Kinds []ArgKind
	// Variadic is the kind for arguments beyond len(Kinds). Only
	// meaningful when Max < 0 or Max > len(Kinds).
	Variadic ArgKind
	// Enum, when non-empty, restricts the first positional argument
	// value (string OR ident) to this set. Used by `@format` to
	// constrain string formats (`email`, `uuid`, ...).
	Enum []string
	// AllowArrayShortcut treats a single array-literal positional arg
	// as variadic-equivalent. Used by `@requiresOneOf(["a","b"])`,
	// `@mimeTypes(["a/b","c/d"])` etc., where humans naturally write
	// the list in brackets. The array's elements are validated against
	// [Variadic]; element count must still satisfy [Min]..[Max].
	AllowArrayShortcut bool
}

ArgsRule captures the positional argument shape of a decorator. Named arguments (`name: value`), nested decorators, and object literals are validated by per-decorator hooks in [analyzer.checkArgsCustom] and do not appear here.

type Diagnostic

type Diagnostic = lexer.Diagnostic

Diagnostic re-exports lexer.Diagnostic so semantic-layer callers do not need to import the lexer package directly.

type FieldCategory added in v1.3.0

type FieldCategory int

FieldCategory classifies a field's resolved type independent of Go syntax.

const (
	CatUnknown   FieldCategory = iota
	CatPrimitive               // string / int* / uint* / float* / bool
	CatBytes                   // the `bytes` builtin (Go []byte)
	CatAny                     // the `any` builtin (Go interface{})
	CatFile                    // the `file` builtin (Go *multipart.FileHeader)
	CatScalar                  // a `scalar Name <prim>` defined type
	CatEnum                    // an `enum Name { ... }` defined type
	CatStruct                  // a `type Name { ... }` struct
	CatArray                   // an array of any of the above
	CatMap                     // a map
)

type Level

type Level uint16

Level is a bitmask of declaration sites where a decorator may appear. A Spec OR-s the levels it accepts; the placement check passes when at least one bit overlaps with the current site. Single-bit values are used for diagnostic rendering - never combine bits when calling Level.Name.

const (
	// LvlFile is a file-header decorator, before `package`. Examples:
	// `@doc("...")`, `@deprecated`.
	LvlFile Level = 1 << iota
	// LvlType is a `type Name { ... }` declaration.
	LvlType
	// LvlField is a field inside a `type` body. Fields inside an
	// `error` body use [LvlErrorField] instead so request-only and
	// validator decorators are rejected on server-emitted payloads.
	LvlField
	// LvlService is a `service Name { ... }` (primary only - `extend
	// service` rejects service-level decorators upstream).
	LvlService
	// LvlMethod is a method inside a service body.
	LvlMethod
	// LvlEnum is an `enum Name { ... }` declaration.
	LvlEnum
	// LvlEnumValue is a single value inside an enum body.
	LvlEnumValue
	// LvlError is an `error Cat Name [{ ... }]` declaration.
	LvlError
	// LvlScalar is a `scalar Name Primitive` declaration.
	LvlScalar
	// LvlMiddleware is a `middleware Name(...)` declaration.
	LvlMiddleware
	// LvlErrorField is a field inside an `error` body. Distinct from
	// [LvlField] because errors are server-emitted, so request-only
	// decorators (`@path`, `@query`, `@body`, `@form`, `@maxSize`,
	// `@mimeTypes`) are rejected. Schema validators (`@minLength`,
	// `@maxLength`, `@pattern`, `@gte`, ...) are accepted but
	// contribute only to OpenAPI schema constraints - codegen does
	// not generate a runtime `Validate()` for ErrorDecl types.
	LvlErrorField
)

func (Level) Name

func (l Level) Name() string

Name returns the label for a single-bit level. It returns "unknown" for the zero value or a multi-bit mask - callers rendering a multi-bit mask should use Level.String instead.

func (Level) String

func (l Level) String() string

String renders every set bit of l joined by ", ", e.g. "field, scalar". Used to format the "@X is only allowed on {levels}" hint. Returns "(none)" for the zero mask so empty Specs surface as a configuration bug rather than a blank message.

type Options

type Options struct {
	// SecuritySchemes lists names declared in the OpenAPI manifest
	// (`craftgo.design.yaml` openapi.securitySchemes). When nil the
	// `@security(name)` reference check is skipped - there is no
	// authoritative list to compare against. When non-nil, every
	// scheme name must appear here or produce a [CodeDecoratorRef]
	// diagnostic. To opt out of inherited security on a public
	// endpoint use `@ignoreSecurity` (not a sentinel scheme name).
	SecuritySchemes []string

	// BasePath is the project's `openapi.basePath` from the manifest.
	// Used by the path-resolution pass to compute final routes for
	// cross-service collision detection and to surface `path/format`
	// warnings on a malformed value. Empty disables basePath checks
	// (as if no basePath were declared).
	BasePath string

	// HealthPaths overrides the default `/healthz`, `/readyz` reserved
	// path set. Empty slice = default; nil also = default. A
	// user-declared method matching one of these paths produces a
	// `path/health-conflict` diagnostic.
	HealthPaths []string

	// DesignRoot is the absolute filesystem path of the project's
	// design folder. When non-empty, [AnalyzeProject] splits files by
	// subdirectory into separate packages and resolves cross-package
	// qualified refs against each file's `import` declarations. When
	// empty (or when calling [Analyze] / [AnalyzeWith]) the analyser
	// behaves as a single-package merge.
	DesignRoot string
	// contains filtered or unexported fields
}

Options configure the analyser's optional cross-reference checks. Pass an empty Options for the default (no truth source for security schemes); the corresponding refs are then silently allowed.

type Package

type Package struct {
	// Name is the package name agreed on by every file with a `package`
	// declaration. Empty when no file has one.
	Name string
	// Types maps `type Name { ... }` declarations by name.
	Types map[string]*ast.TypeDecl
	// Enums maps `enum Name { ... }` declarations by name.
	Enums map[string]*ast.EnumDecl
	// Errors maps `error Cat Name [{ ... }]` declarations by name.
	Errors map[string]*ast.ErrorDecl
	// Scalars maps `scalar Name Primitive` declarations by name.
	Scalars map[string]*ast.ScalarDecl
	// Middlewares maps `middleware Name(...)` declarations by name.
	Middlewares map[string]*ast.MiddlewareDecl
	// Services maps service names to the merged primary + extends bundle.
	Services map[string]*ServiceInfo
}

Package is the merged result of analysing one or more ast.File from the same logical package. The maps are keyed by the unqualified declaration name; cross-package references are resolved by AnalyzeProject.

type Prims

type Prims uint8

Prims is a bitmask of primitive type categories a validator decorator can target. Used by the field-type compatibility check (`@length` only makes sense on strings, `@uniqueItems` only on arrays, etc.). A zero value means "no constraint" - applies to anything, used by metadata decorators like `@doc`.

const (
	// PrimString covers `string` and any scalar whose primitive is
	// string. Bytes/format/uri all reduce to this category.
	PrimString Prims = 1 << iota
	// PrimNumber covers signed/unsigned integers and floats.
	PrimNumber
	// PrimBool covers `bool`.
	PrimBool
	// PrimArray covers `T[]` and `map<K,V>` field shapes (arrays and
	// maps share validation: count, uniqueness).
	PrimArray
	// PrimFile covers the `file` primitive (multipart upload).
	PrimFile
	// PrimAny matches any field type - used by validator-style
	// decorators that don't care about primitive (e.g. `@example`).
	PrimAny Prims = 0
)

func PrimFromName added in v1.2.0

func PrimFromName(name string) Prims

PrimFromName maps a built-in primitive name to its Prims category. Returns 0 for names this layer can't classify (custom types, `any`, `object` - those are handled by the caller). Exported so the LSP reuses the one classification instead of keeping its own copy.

func (Prims) String

func (p Prims) String() string

String renders a Prims set as a comma-joined list for diagnostics. Used in "@length is for string fields, this field is bool" hints.

type Project

type Project struct {
	// Root is the absolute design folder used for filesystem
	// validation of `import "path"`. Empty when AnalyzeProject was
	// called without [Options.DesignRoot] - in that case Packages
	// holds the same single-package result as [Analyze].
	Root string
	// Packages maps `package X` name → analysed [Package].
	Packages map[string]*Package
	// FileImports maps file path → alias → relative import path
	// (path is the value the user wrote in `import "path"`).
	FileImports map[string]map[string]string
}

Project is the cross-package analysis result. Packages is keyed by the package's `package X` declaration name (the value of [Package.Name]), so files in any folder sharing the same name merge into a single entry. FileImports retains the per-file import metadata for LSP "go-to-definition" - at the analysis layer resolution uses package names directly, but the IDE benefits from knowing which folder each `import "path"` referred to.

type ResolvedField added in v1.3.0

type ResolvedField struct {
	// Field is the source field (post generic-substitution / mixin
	// promotion); stages needing the raw decorators or type ref read it here.
	Field *ast.Field

	DSLName  string        // the source field identifier (wire/json base name)
	Category FieldCategory // the resolved type category

	// ResolvedPrim is the underlying DSL primitive: the primitive itself for
	// a primitive/bytes/any field, or the `scalar`'s primitive for a scalar
	// field. "" for enum / struct / array / map / file / unresolved.
	ResolvedPrim string

	// HomePkg is the package the field's named type lives in — the qualifier
	// of a `lib.X` ref, or the package a bare ref was resolved against (which,
	// for a field promoted across a package boundary, is the mixin's home, NOT
	// the using package). "" for a builtin primitive or an unresolved ref.
	HomePkg string

	// IsNilable reports whether the Go type holds nil directly (slice, map,
	// bytes, any, file, or a scalar over a nilable primitive), so an optional
	// `?` / `@nullable` use of it needs no redundant pointer wrap. This is the
	// fact codegen's `*T` decision and the cross-field presence check must
	// agree on.
	IsNilable bool
}

ResolvedField is the layer-agnostic resolved view of one field. It is the floor every stage stands on: a fact read from here cannot disagree with another stage's, because it was computed once.

func ResolveField added in v1.3.0

func ResolveField(f *ast.Field, pkg *Package, proj *Project) ResolvedField

ResolveField computes the layer-agnostic facts for a single field. pkg is the field's HOME package — for a bare named ref it is resolved against pkg, so a field promoted from a sibling-package mixin must be resolved with that mixin's package as pkg (not the using package). proj resolves a qualified `lib.X` ref against its named package.

type ServiceInfo

type ServiceInfo struct {
	Primary *ast.ServiceDecl
	Extends []*ast.ServiceDecl
	Methods []*ast.Method
}

ServiceInfo bundles the primary `service` declaration with every `extend service` continuation that targets the same name. Methods is the merged list in source order.

type Spec

type Spec struct {
	// Name is the bare decorator name (no leading `@`). Stored so callers
	// holding a *Spec can render diagnostics without a separate lookup.
	Name string
	// Levels is the OR of every site where `@Name` is legal. The
	// placement check fails when the current site bit is not set.
	Levels Level
	// Doc is a one-line description shown in LSP hover. Keep it short -
	// the README is the long-form reference.
	Doc string
	// Args is the positional argument shape; the zero value means
	// "no args expected".
	Args ArgsRule
	// AppliesTo restricts the decorator to fields / scalars whose
	// primitive type is in the listed categories. Zero (PrimAny)
	// means no constraint - used by metadata-style decorators. The
	// field-type compatibility check reads this when LvlField or
	// LvlScalar is the current site.
	AppliesTo Prims
	// Flag reports whether the decorator never accepts arguments. It
	// is a presentation hint, not a parser rule:
	//
	//   - LSP completion inserts `@positive` (no parens) for Flag
	//     decorators and `@range($1, $2)` (snippet placeholders) for
	//     the rest.
	//   - `craftgo fmt` strips empty parens (`@positive()` →
	//     `@positive`) so canonical form is parens-free.
	//   - The parser emits [CodeFlagEmptyParens] (warning) when a Flag
	//     decorator is written with empty `()`. Warning only — the
	//     formatter rewrites it on save.
	//
	// The invariant is `Flag == true ⇒ Args.Max == 0`.
	Flag bool
	// Repeatable reports whether multiple `@Name` occurrences on one site
	// are the intended idiom (each adds to an aggregate: tags merge,
	// middlewares chain, security OR-alternatives, errors accumulate) rather
	// than a duplicate. The duplicate-decorator check reads this so the rule
	// lives ONCE here instead of a separate hardcoded list that drifts.
	Repeatable bool
}

Spec describes one decorator: its canonical name, every site it may appear, a short doc string for IDE hover, and the positional argument shape. Every decorator validates through the generic [analyzer.checkPositionalArgs] path - there are no per-decorator argument shape hooks.

func Lookup

func Lookup(name string) (Spec, bool)

Lookup returns the Spec for `name` and whether it is registered. Convenience wrapper kept exported so the LSP / CLI can introspect the registry without poking the bare map.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL