semantic

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: MIT Imports: 9 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).

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.

Cross-package qualified-ref resolution is not yet supported: the analyser uses a folder-merge import model and rejects qualified names. Richer path resolution against [config.OpenAPI].BasePath is also pending. 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"
	// 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"

	// 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"
	// 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"

	// 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"
	// CodeDefaultNeedsOptional fires (severity Warning) when a field
	// carries `@default(...)` but its type lacks the `?` suffix. The
	// formatter auto-adds `?` on save, so the warning clears as soon
	// as the user runs `craftgo fmt` (or format-on-save).
	CodeDefaultNeedsOptional = "decorator/default-needs-optional"
	// 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"
	// 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"

	// 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"
	// 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,
		Doc:    "Example value rendered in the OpenAPI schema for this field. Argument may be a literal (string / int / float / bool) OR a `{k: v}` object.",
		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:  "Logical grouping label for OpenAPI tags & router buckets.",
		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},
	},
	"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},
	},
	"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},
	},
	"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}},
	"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 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

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 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 later (currently out of scope).

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 (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 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`. A future check
	// in init() enforces this.
	Flag 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