swissshepherd

schema_docs rule

The primary rule. Validates argument and attribute documentation against the provider schema.

Sub-checks

All enabled by default; disable individually via the rule’s config block.

Sub-check What it validates
byline First paragraph after section heading matches expected byline text (from type)
coverage Every schema attr is documented; every documented attr exists in schema; every block heading in Argument Reference matches a schema block
deprecated Deprecation status matches between schema and docs (both directions)
description Descriptions don’t start with weak/redundant/meta prefixes (“The “, “This “, “Contains “, “Used “, etc.)
format No code blocks in arg/attr sections; single-line attrs; uninterrupted lists
heading Block headings match the preferred template style
labels Arguments have (Required)/(Optional) labels (and optionally (Read-Only) when allow_inline_read_only = true); attributes do not
ordering Attributes alphabetical (single-byline lists as one group; split required/optional bylines as separate groups)

Config

check "schema_docs" {
  # Sub-check toggles
  byline      = true
  coverage    = true
  deprecated  = true
  description = true
  format      = true
  heading     = true
  labels      = true
  ordering    = true

  # Heading templates (see "Heading templates" docs)
  block_heading_styles        = ["`{Block}` Block", "{Block}", "{Title}"]
  prefer_block_heading_styles = ["`{Block}` Block"]

  # Coverage options
  ignore_deprecated      = true                     # skip deprecated schema attrs
  implicit_attributes    = ["id", "tags_all"]       # never flagged as undocumented
  allow_phantoms         = ["tags", "tags_all"]     # never flagged as phantom
  skip_blocks            = ["timeouts"]             # blocks skipped entirely
  allow_inline_read_only = false                    # see "Schema model" below

  # Description options
  # Overrides the default weak/redundant/meta starts. Default:
  #   "A ", "An ", "The ", "This ", "It ",
  #   "Indicates ", "Specifies ", "Describes ", "Defines ",
  #   "Contains ", "Determines ", "Identifies ", "Represents ", "Denotes ", "Holds ", "Used "
  bad_prefixes = ["A ", "An ", "The ", "This ", "Specifies "]

  # Format options
  no_code_blocks              = true   # no fenced code blocks in arg/attr sections
  single_line_attrs           = true   # each attribute on one line
  uninterrupted_lists         = true   # no paragraphs between list items
  allow_attribute_indentation = true   # allow indented sub-attributes in Attribute Reference (default: true)

  # Object-typed attribute coverage (opt-in; default false)
  nested_object_attributes    = true   # check fields of list(object)/set(object)/object attributes
}

Object-typed attributes (nested_object_attributes)

Legacy list(object({...})) / set(object({...})) / bare object({...}) attributes carry their fields as a nested type, not as a schema block. By default swissshepherd treats such an attribute as a single leaf: its inner fields are neither required to be documented nor style-checked.

Set nested_object_attributes = true to model those fields as nested blocks. When enabled:

The cty type encoding of an object (list/set/map(object({...})), object) records field names and types but no per-field Required/Optional/Computed. swissshepherd therefore uses the parent attribute’s configurability:

This is off by default because enabling it surfaces a large number of new findings in docs that previously passed. Roll it out gradually — stage with ignore_targets / skip_blocks, or enable it once the affected docs are clean.

For the full rationale behind the parent-configurability model and the ConfigUnknown flag — why some object fields cannot be classified into a specific section, the proto5 constraint that causes it, and the future-alignment path — see Object-typed attributes, proto5, and the ConfigUnknown escape hatch.

Nested fields documented under a shared or prose-introduced subsection

Two long-standing documentation conventions attach a nested block’s fields to it without a dedicated per-path heading. When nested_object_attributes is enabled, coverage understands both:

Schema model: Required / Optional / Read-Only

The coverage sub-check enforces presence of every schema attribute at every depth of nesting. swissshepherd uses the same three-category mental model as tfplugindocs:

For nested blocks, Read-Only attributes can be documented in any of the following equivalent forms:

The default (allow_inline_read_only = false) preserves the AWS provider’s traditional separation: ## Argument Reference for configurable attributes, ## Attribute Reference for Read-Only ones. Setting the toggle to true permits the tfplugindocs-aligned permissive convention without requiring all docs to convert at once.

Coverage: phantom block headings

The coverage sub-check also flags H3+ headings inside ## Argument Reference whose name doesn’t match any schema block. Common patterns this catches:

The check is restricted to ## Argument Reference. Block-style headings under ## Attribute Reference (e.g. ### Endpoint, ### master_user_secret) commonly document the structure of computed attributes that have no Block representation in the schema, and are not flagged.

Ordering: single vs. split lists

The ordering sub-check adapts to how arguments are presented in the doc:

The signal comes directly from the byline text, not the order of attributes. Mixing labels in a single-list block (e.g., a (Required) after several (Optional) items) is allowed as long as the names are alphabetical overall.