///|
/// `Mixins = [SP] "with" [WS] "[" [WS] 1*(ShapeId [WS]) "]"`. `[SP]`, not
/// `[WS]` or `[BR]`: right after a top-level shape's name (or its
/// `ForResource`), `with` on the next line is not a `Mixins` clause (the
/// shape has none), so the caller's `BR` check runs into it as a new
/// statement instead ("Unknown shape type: with", matching the reference).
/// `same_line` is `false` for an inline shape (`:=`), where `with` may
/// follow a trait that ends its own line (the reference's
/// `valid/inline-io.smithy`: `input :=\n @sensitive\n with [M] {...}`).
fn Parser::parse_mixins(
self : Parser,
same_line~ : Bool = true,
) -> @ast.Mixins? raise ParseError {
guard self.at_keyword("with") && (!same_line || !self.has_br_before()) else {
return None
}
let kw = self.expect_kind(Identifier, "`with`")
self.require_v2("mixins", kw.span)
ignore(self.expect_kind(LBracket, "`[` after `with`"))
let ids : Array[@ast.Node[@ast.ShapeId]] = []
while !self.at_kind(RBracket) {
if self.at_end() {
self.fail(
"SM-PARSE-002",
"UNFINISHED MIXINS",
"I reached the end of the file inside the mixin list",
self.here(),
hint="Add the closing `]`.",
)
}
ids.push(self.parse_shape_id())
}
let close = self.expect_kind(RBracket, "`]`")
if ids.is_empty() {
self.fail(
"SM-PARSE-001",
"EMPTY MIXINS",
"`with [ ]` needs at least one mixin shape ID",
kw.span.join(close.span),
example="with [MyMixin]",
)
}
Some({ ids: ids[:], span: kw.span.join(close.span), })
}
///|
/// `ForResource = SP "for" SP ShapeId`.
fn Parser::parse_for_resource(
self : Parser,
) -> @ast.Node[@ast.ShapeId]? raise ParseError {
guard self.at_keyword("for") else { return None }
let kw = self.expect_kind(Identifier, "`for`")
self.require_v2("resource_binding", kw.span)
Some(self.parse_shape_id())
}
///|
/// The identifier after a shape keyword. It must be on the keyword's own
/// line, and it must be a plain identifier: the reference reports
/// `string com.foo#Bam` at the dot.
fn Parser::parse_shape_name(
self : Parser,
kw : @scanner.Token,
) -> @ast.Node[String] raise ParseError {
let keyword = kw.lexeme
guard self.peek() is Some(t) && t.kind == Identifier else {
self.fail(
"SM-PARSE-001",
"MISSING SHAPE NAME",
"I was expecting a shape name after `\{keyword}` but found \{self.found()}",
self.here_anchored(),
example="\{keyword} MyShape",
)
}
if kw.trivia_after.iter().any(x => x.is_newline()) ||
t.trivia_before.iter().any(x => x.is_newline()) {
// Report at the line break itself (the reference's own unexpected
// token), not at the shape name that follows it.
let break_span = match
kw.trivia_after.iter().find_first(x => x.is_newline()) {
Some(piece) => piece.span()
None =>
match t.trivia_before.iter().find_first(x => x.is_newline()) {
Some(piece) => piece.span()
None => self.here()
}
}
self.fail(
"SM-PARSE-001",
"MISSING SHAPE NAME",
"I was expecting a shape name after `\{keyword}` on the same line but found a line break",
break_span,
hint="Write the name right after the keyword: `\{keyword} Name`.",
)
}
let name = self.expect_identifier("a shape name")
if (self.at_kind(Dot) || self.at_kind(Pound) || self.at_kind(Dollar)) &&
self.adjacent() {
// Read the rest of the ID to show it whole.
let start = name.span.start
let mut end = name.span.end()
while (
self.at_kind(Dot) ||
self.at_kind(Pound) ||
self.at_kind(Dollar) ||
self.at_kind(Identifier)
) &&
self.adjacent() {
match self.advance() {
Some(t) => end = t.span.end()
None => ()
}
}
let shown = self.text_between(start, end)
let reason = if shown.contains("#") {
"has a namespace"
} else if shown.contains("$") {
"names a member"
} else {
"has a dot"
}
self.fail(
"SM-PARSE-005",
"INVALID SHAPE NAME",
"a shape name is a plain identifier, but `\{shown}` \{reason}",
start.to(end),
notes=[
"A shape is defined in the file's namespace, so its name carries no namespace, and members are declared inside the shape.",
],
hint="Write `\{keyword} \{name.value}`.",
)
}
name
}
///|
/// The source text between two offsets (from the tokens' lexemes).
fn Parser::text_between(
self : Parser,
start : @span.Offset,
end : @span.Offset,
) -> String {
let sb = StringBuilder()
for t in self.tokens {
if t.span.start >= start && t.span.end() <= end {
sb.write_string(t.lexeme)
}
}
sb.to_string()
}
///|
/// Whether `peek()` starts a member: a trait, an identifier, or `$`.
fn Parser::at_member_start(self : Parser) -> Bool {
self.at_kind(At) || self.at_kind(Identifier) || self.at_kind(Dollar)
}
///|
/// Whether the token at `i` is a shape keyword, `apply`, `use`, `metadata`
/// or `namespace` at column 1. These are the only tokens whose appearance
/// inside an unclosed block means the block was left open: `@`, `$` and a
/// doc comment are themselves legal member/trait starts at any column
/// (including column 1), so they never end a block on their own. A
/// keyword followed by `:` never starts a statement: it is a member
/// named like one (`namespace: String`).
fn Parser::at_block_breaking_statement(self : Parser, i : Int32) -> Bool {
guard self.first_on_line(i) && self.at_column_one(i) else { return false }
guard self.tokens[i].kind == Identifier &&
statement_keyword(self.tokens[i].lexeme) else {
return false
}
// The token after the keyword, holes skipped, as `peek1` does.
let mut next = i + 1
while next < self.tokens.length() && self.tokens[next].kind == Error {
next += 1
}
!(next < self.tokens.length() && self.tokens[next].kind == Colon)
}
///|
/// The "unfinished block" diagnostic shared by every block kind: a member
/// block, an enum block, an operation body or an apply block. Checked at
/// the top of each block's loop, before parsing the next item.
fn Parser::check_unfinished(
self : Parser,
title : String,
owner : String,
) -> Unit raise ParseError {
if self.at_block_breaking_statement(self.pos) {
self.fail(
"SM-PARSE-001",
title,
"I was reading \{owner} and found \{self.found()} at the start of a line; did you forget `}`?",
self.here(),
hint="Close the block with `}` before the next statement.",
)
}
if self.at_end() {
self.fail(
"SM-PARSE-002",
title,
"I reached the end of the file while reading \{owner}",
self.here(),
hint="Add the closing `}`.",
)
}
}
///|
/// Whether, starting at `i`, skipping over a run of trait applications
/// (`@Name` or `@Name(...)`, balancing parens), the next token is a
/// column-1 shape keyword. When it is, the traits at `i` are not a new
/// member's leading traits: the block was left open, and what looks
/// like a member start is really the start of the *next* shape.
fn Parser::leads_to_block_break(self : Parser, i : Int32) -> Bool {
let mut j = i
while j < self.tokens.length() && self.tokens[j].kind == At {
j += 1
if j < self.tokens.length() && self.tokens[j].kind == Identifier {
j += 1
while j + 1 < self.tokens.length() &&
(
self.tokens[j].kind == Dot ||
self.tokens[j].kind == Pound ||
self.tokens[j].kind == Dollar
) &&
self.tokens[j + 1].kind == Identifier {
j += 2
}
}
if j < self.tokens.length() && self.tokens[j].kind == LParen {
let mut depth = 1
j += 1
while j < self.tokens.length() && depth > 0 {
if self.tokens[j].kind == LParen {
depth += 1
}
if self.tokens[j].kind == RParen {
depth -= 1
}
j += 1
}
}
}
self.at_block_breaking_statement(j)
}
///|
/// Skip to the next member start or the closing brace after an error in a
/// member, so one bad member does not drop the shape. `start_pos` is
/// `self.pos` from before the failed item was attempted: when the item
/// consumed nothing at all (`self.pos` is still `start_pos`), the current
/// token is itself the thing that failed to parse, so it is always
/// advanced past before looking for a stop point — otherwise, if that
/// token already satisfies a stop condition (an indented `$member`, say),
/// this would return without moving and the caller would retry the same
/// token forever. When the item consumed something before failing,
/// `self.pos` is left exactly where it should resume (often the very next
/// member's first token, e.g. after `a:` fails to find a target and the
/// next token is `@required`), so nothing is skipped. Returns `true` when
/// it stops at `}` or at any first-on-line member start (`@`, `$`, or an
/// identifier), so the caller may record the diagnostic and keep going;
/// `false` when it instead reaches a block-breaking statement
/// (`at_block_breaking_statement`) or the end of the file, so the caller
/// must re-raise the original error instead of reporting a second one.
fn Parser::recover_member(self : Parser, start_pos : Int32) -> Bool {
if self.pos == start_pos && !self.at_end() && !self.at_kind(RBrace) {
ignore(self.advance())
}
while !self.at_end() && !self.at_kind(RBrace) {
if self.at_block_breaking_statement(self.pos) {
return false
}
if self.first_on_line(self.pos) && self.at_member_start() {
if self.tokens[self.pos].kind == At && self.leads_to_block_break(self.pos) {
return false
}
return true
}
ignore(self.advance())
}
self.at_kind(RBrace)
}
///|
/// The `WS` between two members: the grammar is `ShapeMembers = "{" [WS]
/// *(ShapeMember [WS]) "}"`, and `WS = 1*(SP / NL / Comment / Comma)`, so
/// any whitespace at all separates members (unlike a statement's `BR`,
/// members do not need a line break, a comment or a comma specifically:
/// `enum A { FOO BAR BAZ }` and `map H { key: String value: String }` are
/// both valid, with members separated only by a space).
fn Parser::expect_member_br(self : Parser) -> Unit raise ParseError {
guard self.pos > 0 else { return }
guard self.peek() is Some(next) else { return }
let prev = self.tokens[self.pos - 1]
let has_sep = prev.trivia_after.length() > 0 ||
next.trivia_before.length() > 0
if !has_sep {
self.fail(
"SM-PARSE-003",
"MISSING SEPARATOR",
"I was expecting whitespace here but found \{self.found()}",
next.span,
notes=["Each member must be separated from the next by whitespace."],
hint="Put a space before `\{next.lexeme}`.",
)
}
}
///|
/// The shared loop for `ShapeMembers` and `EnumShapeMembers`: read items
/// with `parse_item` until `}`, recovering from one bad item (without
/// losing the rest) and restoring `contexts` and `depth` to what they were
/// before the bad item, since whatever it entered (a trait body, a node
/// value) may never have run its own cleanup once it raised.
fn[T] Parser::parse_block_items(
self : Parser,
title : String,
owner : String,
parse_item : () -> Result[T, ParseError],
needs_br? : (T) -> Bool,
name_of? : (T) -> @ast.Node[String],
) -> Array[T] raise ParseError {
let items : Array[T] = []
let seen_names : Array[String] = []
while !self.at_kind(RBrace) {
self.check_unfinished(title, "the members of \{owner}")
let start_pos = self.pos
let ctx_len = self.contexts.length()
let depth0 = self.depth
match parse_item() {
Ok(m) => {
let strict = match needs_br {
Some(f) => f(m)
None => false
}
match name_of {
Some(f) => {
let name = f(m)
// Reported, not raised: the shape and both members stay in
// the AST, so editors and generators keep the shape.
if seen_names.contains(name.value) {
self.report(
"SM-PARSE-014",
Error,
"DUPLICATE MEMBER",
"Found conflicting member name, `\{name.value}`",
name.span,
notes=[
"I already saw a member named `\{name.value}` in this shape; each member name is used once.",
],
hint="Rename one of them or remove the duplicate.",
)
}
seen_names.push(name.value)
}
None => ()
}
items.push(m)
if !self.at_kind(RBrace) {
// `ValueAssignment` ends with a true `BR`, stricter than the
// `[WS]` that otherwise separates members (`FOO = 1 BAR = 2` on
// one line is invalid, but `FOO BAR` is fine).
if strict {
self.expect_br()
} else {
self.expect_member_br()
}
}
}
Err(e) => {
while self.contexts.length() > ctx_len {
ignore(self.contexts.pop())
}
self.depth = depth0
if self.recover_member(start_pos) {
self.diagnostics.push(e.diagnostic())
} else {
raise e
}
}
}
}
self.detach_docs_before(self.here().start)
items
}
///|
/// `ShapeMembers = "{" [WS] *(ShapeMember [WS]) "}"`, each
/// `ShapeMember = TraitStatements (ExplicitShapeMember / ElidedShapeMember) [ValueAssignment]`.
fn Parser::parse_members(
self : Parser,
owner : String,
) -> Array[@ast.Member] raise ParseError {
let open = self.expect_kind(LBrace, "`{` to start the members of \{owner}")
self.enter()
let members = self.parse_block_items(
"UNFINISHED SHAPE",
owner,
() => {
try Ok(self.parse_member()) catch {
e => Err(e)
} noraise {
v => v
}
},
needs_br=m => m.default is Some(_),
name_of=m => m.name,
)
ignore(self.expect_kind(RBrace, "`}`"))
self.leave()
ignore(open)
members
}
///|
/// One `ShapeMember`.
fn Parser::parse_member(self : Parser) -> @ast.Member raise ParseError {
guard self.peek() is Some(first) else {
self.fail(
"SM-PARSE-002",
"UNEXPECTED END OF FILE",
"I was expecting a member",
self.here(),
)
}
let docs = self.take_docs_before(first.span.start)
let traits = self.parse_traits()
// Docs between traits and the name still attach.
docs.append(self.take_docs_before(self.here().start))
// A column-1 shape keyword after a member's leading traits means those
// traits belong to the *next* shape, not to a member here: the block
// was left open, not a member with an odd name.
if self.at_block_breaking_statement(self.pos) {
self.fail(
"SM-PARSE-001",
"UNFINISHED SHAPE",
"I was reading a member and found \{self.found()} at the start of a line; did you forget `}`?",
self.here(),
hint="Close the block with `}` before the next statement.",
)
}
let (name, target) : (@ast.Node[String], @ast.MemberTarget) = if self.at_kind(
Dollar,
) {
let d = self.expect_kind(Dollar, "`$`")
self.require_v2("elision", d.span)
guard self.adjacent() && self.at_kind(Identifier) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting a member name right after `$` but found \{self.found()}",
self.here(),
example="$memberName",
)
}
let n = self.expect_identifier("a member name")
(@ast.Node::new(n.value, d.span.join(n.span)), Elided)
} else {
guard self.at_kind(Identifier) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting a member name but found \{self.found()}",
self.here(),
notes=[
"A member looks like `name: Target`, with optional traits before it and an optional `= default` after it.",
],
)
}
let n = self.expect_identifier("a member name")
guard self.at_kind(Colon) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting `:` after the member name `\{n.value}` but found \{self.found()}",
self.here(),
example="\{n.value}: String",
)
}
ignore(self.advance())
let target = self.parse_shape_id()
(n, Explicit(target))
}
let mut end = match target {
Explicit(t) => t.span.end()
Elided => name.span.end()
}
let default : @ast.NodeValue? = if self.at_kind(Equal) {
let eq = self.expect_kind(Equal, "`=`")
self.require_v2("default", eq.span)
self.expect_sp_after("the default value")
let v = self.parse_node_value()
end = v.span().end()
Some(v)
} else {
None
}
let start = if traits.length() > 0 {
traits[0].span.start
} else {
name.span.start
}
{
docs: docs[:],
traits: traits[:],
name,
target,
default,
span: start.to(end),
}
}
///|
/// `EnumShapeMembers = "{" [WS] 1*(EnumShapeMember [WS]) "}"`.
fn Parser::parse_enum_members(
self : Parser,
owner : String,
) -> Array[@ast.EnumMember] raise ParseError {
ignore(self.expect_kind(LBrace, "`{` to start the members of \{owner}"))
self.enter()
let members = self.parse_block_items(
"UNFINISHED ENUM",
owner,
() => {
try Ok(self.parse_enum_member()) catch {
e => Err(e)
} noraise {
v => v
}
},
needs_br=m => m.value is Some(_),
name_of=m => m.name,
)
ignore(self.expect_kind(RBrace, "`}`"))
self.leave()
members
}
///|
/// One `EnumShapeMember`.
fn Parser::parse_enum_member(self : Parser) -> @ast.EnumMember raise ParseError {
guard self.peek() is Some(first) else {
self.fail(
"SM-PARSE-002",
"UNEXPECTED END OF FILE",
"I was expecting an enum member",
self.here(),
)
}
let docs = self.take_docs_before(first.span.start)
let traits = self.parse_traits()
docs.append(self.take_docs_before(self.here().start))
if self.at_kind(Dollar) {
let d = self.expect_kind(Dollar, "`$`")
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"an enum member is a name, not `$name`",
d.span,
hint="Write the member name without a leading `$`; `$name` elides a member from a mixin, which enums don't have.",
)
}
guard self.at_kind(Identifier) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting an enum member name but found \{self.found()}",
self.here(),
example="RED = \"red\"",
)
}
let name = self.expect_identifier("an enum member name")
let mut end = name.span.end()
let value : @ast.NodeValue? = if self.at_kind(Equal) {
ignore(self.advance())
self.expect_sp_after("the enum value")
let v = self.parse_node_value()
end = v.span().end()
Some(v)
} else {
None
}
let start = if traits.length() > 0 {
traits[0].span.start
} else {
name.span.start
}
{ docs: docs[:], traits: traits[:], name, value, span: start.to(end), }
}
///|
/// `InlineAggregateShape = ":=" [WS] TraitStatements [ForResource] [Mixins] [WS] ShapeMembers`.
fn Parser::parse_inline_shape(
self : Parser,
walrus : @scanner.Token,
) -> @ast.InlineShape raise ParseError {
self.require_v2("inline", walrus.span)
let docs = self.take_docs_before(walrus.span.start)
let traits = self.parse_traits()
let for_resource = self.parse_for_resource()
let mixins = self.parse_mixins(same_line=false)
self.push_context("an inline shape", walrus.span)
let members = self.parse_members("the inline shape")
self.pop_context()
let end = self.tokens[self.pos - 1].span.end()
{
docs: docs[:],
traits: traits[:],
for_resource,
mixins,
members: members[:],
span: walrus.span.start.to(end),
}
}
///|
/// `OperationBody = "{" [WS] *(OperationProperty [WS]) "}"`.
fn Parser::parse_operation_body(
self : Parser,
owner : String,
) -> Array[@ast.OperationProperty] raise ParseError {
ignore(self.expect_kind(LBrace, "`{` to start the body of \{owner}"))
self.enter()
let props : Array[@ast.OperationProperty] = []
while !self.at_kind(RBrace) {
self.check_unfinished("UNFINISHED OPERATION", owner)
// Docs before `input :=` belong to the inline shape; elsewhere they are detached.
guard self.peek() is Some(key) && key.kind == Identifier else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting `input`, `output` or `errors` but found \{self.found()}",
self.here(),
)
}
let duplicate = match key.lexeme {
"input" => props.iter().any(p => p is Input(_, _))
"output" => props.iter().any(p => p is Output(_, _))
"errors" => props.iter().any(p => p is Errors(_, _))
_ => false
}
if duplicate {
self.report(
"SM-PARSE-012",
Error,
"DUPLICATE OPERATION PROPERTY",
"Duplicate operation \{key.lexeme} property for \{owner}",
key.span,
notes=[
"I already saw a `\{key.lexeme}` property for \{owner}; an operation body holds `input`, `output` and `errors`, each at most once.",
],
hint="Remove the duplicate `\{key.lexeme}` property.",
)
}
match key.lexeme {
"input" | "output" => {
ignore(self.advance())
let target : @ast.OperationTarget = if self.at_kind(Walrus) {
let w = self.expect_kind(Walrus, "`:=`")
Inline(self.parse_inline_shape(w))
} else {
self.detach_docs_before(key.span.start)
ignore(self.expect_kind(Colon, "`:` or `:=` after `\{key.lexeme}`"))
Reference(self.parse_shape_id())
}
let end = self.tokens[self.pos - 1].span.end()
let span = key.span.start.to(end)
props.push(
if key.lexeme == "input" {
Input(target, span)
} else {
Output(target, span)
},
)
}
"errors" => {
self.detach_docs_before(key.span.start)
ignore(self.advance())
ignore(self.expect_kind(Colon, "`:` after `errors`"))
ignore(self.expect_kind(LBracket, "`[` to start the error list"))
let ids : Array[@ast.Node[@ast.ShapeId]] = []
while !self.at_kind(RBracket) {
if self.at_end() {
self.fail(
"SM-PARSE-002",
"UNFINISHED ERROR LIST",
"I reached the end of the file inside the error list",
self.here(),
hint="Add the closing `]`.",
)
}
ids.push(self.parse_shape_id())
}
let close = self.expect_kind(RBracket, "`]`")
props.push(Errors(ids[:], key.span.join(close.span)))
}
other =>
self.fail(
"SM-PARSE-011",
"UNKNOWN OPERATION PROPERTY",
"an operation has no `\{other}` property",
key.span,
notes=[
"An operation body holds `input`, `output` and `errors`, each at most once.",
],
hint=if other == "inputs" {
"Did you mean `input`?"
} else if other == "outputs" {
"Did you mean `output`?"
} else if other == "error" {
"Did you mean `errors`?"
} else {
"Remove it, or move it into a trait."
},
)
}
if !self.at_kind(RBrace) {
self.expect_member_br()
}
}
self.detach_docs_before(self.here().start)
ignore(self.expect_kind(RBrace, "`}`"))
self.leave()
props
}
///|
/// The value of the node object key `key` in `body`, if `body` is an
/// object and has it.
fn object_key(body : @ast.NodeValue, key : String) -> @ast.NodeValue? {
guard body is Object(kvps, _) else { return None }
for kvp in kvps {
if kvp.key.value == key {
return Some(kvp.value)
}
}
None
}
///|
/// `Shape`, after the docs and traits; `start` is the statement start.
fn Parser::parse_shape(
self : Parser,
docs : Array[@ast.DocComment],
traits : Array[@ast.Trait],
start : @span.Offset,
) -> @ast.ShapeStatement raise ParseError {
guard self.peek() is Some(kw) &&
kw.kind == Identifier &&
@ast.ShapeKind::of(kw.lexeme) is Some(kind) else {
if traits.length() > 0 || docs.length() > 0 {
self.fail(
"SM-PARSE-004",
"EXPECTED A SHAPE",
"I was expecting a shape after the traits but found \{self.found()}",
self.here(),
notes=[
"Traits and documentation comments apply to the shape statement right after them.",
],
)
}
self.fail(
"SM-PARSE-004",
"EXPECTED A STATEMENT",
"I was expecting a shape statement or `apply` but found \{self.found()}",
self.here(),
)
}
ignore(self.advance())
let name = self.parse_shape_name(kw)
let owner = "the \{kw.lexeme} `\{name.value}`"
self.push_context(owner, kw.span)
let shape : @ast.Shape = match kind {
Simple(t) => Simple(kind=t, name~, mixins=self.parse_mixins())
Enum(t) => {
self.require_v2("enum", kw.span)
let mixins = self.parse_mixins()
let members = self.parse_enum_members(owner)
Enum(kind=t, name~, mixins~, members=members[:])
}
Aggregate(t) => {
// `set` is not a shape type keyword in IDL 2.0 (lists took over,
// with `@uniqueItems` for the old `set` behavior). Like
// `require_v2`, this reports once per file.
if t is Set && self.claim_v1_report() {
self.report(
"SM-PARSE-015",
Error,
"NEEDS SMITHY IDL 1",
"set shapes cannot be used in Smithy version 2.0",
kw.span,
notes=["Use a list with the `@uniqueItems` trait in IDL 2.0 instead."],
)
}
let for_resource = self.parse_for_resource()
let mixins = self.parse_mixins()
let members = self.parse_members(owner)
Aggregate(kind=t, name~, for_resource~, mixins~, members=members[:])
}
Entity(t) => {
let mixins = self.parse_mixins()
guard self.at_kind(LBrace) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting `{` to start the body of \{owner} but found \{self.found()}",
self.here(),
example="\{kw.lexeme} \{name.value} {\n version: \"2024-01-01\"\n}",
)
}
let body = self.parse_node_value()
if t is Resource {
if object_key(body, "properties") is Some(_) && self.claim_v2_report() {
self.report(
"SM-PARSE-007",
Error,
"NEEDS SMITHY IDL 2",
"Resource properties can only be used with Smithy version 2 or later",
body.span(),
notes=[
"This file is Smithy IDL 1.0: " +
(if self.version is None {
"it has no `$version` statement, so I read it as version 1.0."
} else {
"its `$version` says so."
}),
],
hint="Add `$version: \"2\"` as the first line of the file.",
)
}
}
Entity(kind=t, name~, mixins~, body~)
}
Operation => {
let mixins = self.parse_mixins()
let properties = self.parse_operation_body(owner)
Operation(name~, mixins~, properties=properties[:])
}
}
self.pop_context()
let end = self.tokens[self.pos - 1].span.end()
{ docs: docs[:], traits: traits[:], shape, span: start.to(end), }
}
///|
/// `ApplyStatement = ApplyStatementSingular / ApplyStatementBlock`.
fn Parser::parse_apply(
self : Parser,
start : @span.Offset,
) -> @ast.ApplyStatement raise ParseError {
let kw = self.expect_kind(Identifier, "`apply`")
self.push_context("an apply statement", kw.span)
let target = self.parse_shape_id()
self.expect_sp_present("the trait or `{` after the apply target")
let (traits, block) : (Array[@ast.Trait], Bool) = if self.at_kind(LBrace) {
ignore(self.advance())
self.enter()
let ts : Array[@ast.Trait] = []
while !self.at_kind(RBrace) {
self.check_unfinished(
"UNFINISHED APPLY",
"the traits applied to `\{target.value}`",
)
ts.push(self.parse_trait())
}
self.detach_docs_before(self.here().start)
ignore(self.expect_kind(RBrace, "`}` to close the apply block"))
self.leave()
(ts, true)
} else {
guard self.at_kind(At) else {
self.fail(
"SM-PARSE-001",
"UNEXPECTED TOKEN",
"I was expecting a trait or `{` after the apply target but found \{self.found()}",
self.here(),
example="apply \{target.value} @deprecated",
)
}
([self.parse_trait()], false)
}
self.pop_context()
let end = self.tokens[self.pos - 1].span.end()
{ target, traits: traits[:], block, span: start.to(end), }
}
///|
/// `ShapeOrApplyStatement`, then `BR`.
fn Parser::parse_shape_or_apply(
self : Parser,
) -> @ast.Statement raise ParseError {
guard self.peek() is Some(first) else {
self.fail(
"SM-PARSE-002",
"UNEXPECTED END OF FILE",
"I was expecting a statement",
self.here(),
)
}
let start = first.span.start
let docs = self.take_docs_before(start)
if self.at_keyword("apply") {
if docs.length() > 0 {
// An `apply` statement adds traits to a shape defined elsewhere, so it
// cannot take documentation: the reference treats a doc comment here
// as an ordinary comment (dropped, not an error).
self.report(
"SM-PARSE-008",
Warning,
"DOCS BEFORE APPLY",
"a documentation comment cannot be applied with an apply statement, so I read it as an ordinary comment",
docs[0].span,
notes=[
"An `apply` statement adds traits to a shape defined elsewhere; documentation is written where the shape is defined, or applied as `@documentation(\"...\")`.",
],
hint="Turn it into a `//` comment or into `apply \{self.peek1().map(t => t.lexeme).unwrap_or("Target")} @documentation(\"...\")`.",
)
}
let a = self.parse_apply(start)
self.expect_br()
return Apply(a)
}
let traits = self.parse_traits()
// Only a shape type keyword after the traits belongs to this statement
// for recovery; anything else (`apply`, say) is a statement of its own.
if self.peek() is Some(k) &&
k.kind == Identifier &&
@ast.ShapeKind::of(k.lexeme) is Some(_) {
self.keyword_index = self.pos
}
docs.append(self.take_docs_before(self.here().start))
let s = self.parse_shape(docs, traits, start)
self.expect_br()
Shape(s)
}