///|
/// A read-only reference to one node of a parse result. Each case holds the
/// AST value it points to (nothing is copied). Generic code uses `category`,
/// `kind`, `span`, `children` and `field`; typed code matches the cases.
///
/// Kinds and field names follow the Smithy JSON AST where it has a name (a
/// shape's kind is its `type`, a member has a `target`), and the IDL grammar
/// for the rest. `(category, kind)` identifies a node type.
///
/// The file node also holds the comments that the AST does not keep: every
/// `//` comment and every `///` comment that documents nothing.
///
/// ```mbt check
/// test {
/// let r = @parser.parse_text(
/// (
/// #|$version: "2"
/// #|namespace a
/// #|
/// #|string Name
/// #|
/// ),
/// )
/// let root = @traversal.NodeRef::of_result(r).unwrap()
/// inspect("\{root.category()}/\{root.kind()}", content="file/file")
/// guard root is File(file, _) else { fail("not a file") }
/// inspect(file.statements.length(), content="1")
/// }
/// ```
pub(all) enum NodeRef {
File(@ast.IdlFile, ArrayView[@scanner.Comment])
Control(@ast.ControlStatement)
Metadata(@ast.MetadataStatement)
Namespace(@ast.NamespaceStatement)
Use(@ast.UseStatement)
Shape(@ast.ShapeStatement)
Apply(@ast.ApplyStatement)
Inline(@ast.InlineShape)
Mixins(@ast.Mixins)
Member(@ast.Member)
EnumMember(@ast.EnumMember)
Property(@ast.OperationProperty)
Trait(@ast.Trait)
Value(@ast.NodeValue)
Entry(@ast.NodeKvp)
ShapeIdRef(@ast.Node[@ast.ShapeId])
Name(@ast.Node[String])
Doc(@ast.DocComment)
Comment(@scanner.Comment)
} derive(Eq, Debug)
///|
/// The file node of `result`, or `None` when the parser read no AST. The
/// comments come from the CST trivia, in source order, without the doc
/// comments that the AST attaches to a shape, member or inline shape.
pub fn NodeRef::of_result(result : @parser.ParseResult) -> NodeRef? {
guard result.ast is Some(file) else { return None }
let attached : Set[@span.Span] = Set([])
fn attach(docs : ArrayView[@ast.DocComment]) {
for d in docs {
attached.add(d.span)
}
}
for s in file.statements {
guard s is Shape(st) else { continue }
attach(st.docs)
match st.shape {
Enum(members~, ..) =>
for m in members {
attach(m.docs)
}
Aggregate(members~, ..) =>
for m in members {
attach(m.docs)
}
Operation(properties~, ..) =>
for p in properties {
if p is (Input(Inline(i), _) | Output(Inline(i), _)) {
attach(i.docs)
for m in i.members {
attach(m.docs)
}
}
}
_ => ()
}
}
let comments : Array[@scanner.Comment] = []
fn gather(trivia : ArrayView[@scanner.Trivia]) {
for t in trivia {
if t is Comment(c) && !(c.kind == Doc && attached.contains(c.span)) {
comments.push(c)
}
}
}
gather(result.cst.trivia)
for token in result.cst.tokens {
gather(token.trivia_before)
gather(token.trivia_after)
}
Some(File(file, comments[:]))
}
///|
/// The node's category: `file`, `control`, `metadata`, `namespace`, `use`,
/// `shape`, `mixins`, `member`, `operation_property`, `trait`, `value`,
/// `entry`, `shape_id`, `name`, `doc` or `comment`.
pub fn NodeRef::category(self : NodeRef) -> String {
match self {
File(_, _) => "file"
Control(_) => "control"
Metadata(_) => "metadata"
Namespace(_) => "namespace"
Use(_) => "use"
Shape(_) | Apply(_) | Inline(_) => "shape"
Mixins(_) => "mixins"
Member(_) | EnumMember(_) => "member"
Property(_) => "operation_property"
Trait(_) => "trait"
Value(_) => "value"
Entry(_) => "entry"
ShapeIdRef(_) => "shape_id"
Name(_) => "name"
Doc(_) => "doc"
Comment(_) => "comment"
}
}
///|
/// The JSON AST `type` of a shape: its IDL keyword.
fn shape_kind(shape : @ast.Shape) -> String {
match shape {
Simple(kind~, ..) => kind.keyword()
Enum(kind=Enum, ..) => "enum"
Enum(kind=IntEnum, ..) => "intEnum"
Aggregate(kind=List, ..) => "list"
Aggregate(kind=Map, ..) => "map"
Aggregate(kind=Union, ..) => "union"
Aggregate(kind=Structure, ..) => "structure"
Aggregate(kind=Set, ..) => "set"
Entity(kind=Service, ..) => "service"
Entity(kind=Resource, ..) => "resource"
Operation(..) => "operation"
}
}
///|
/// The node's kind within its category. A shape's kind is its JSON AST
/// `type` (`structure`, `intEnum`, `bigDecimal`, …, or `apply` and
/// `inline`); a value's kind is its JSON node type (`boolean` for `true` and
/// `false`), or `textBlock` and `shapeId`.
pub fn NodeRef::kind(self : NodeRef) -> String {
match self {
Shape(s) => shape_kind(s.shape)
Apply(_) => "apply"
Inline(_) => "inline"
Member(_) => "member"
EnumMember(_) => "enumMember"
Property(Input(_, _)) => "input"
Property(Output(_, _)) => "output"
Property(Errors(_, _)) => "errors"
Value(Array(_, _)) => "array"
Value(Object(_, _)) => "object"
Value(Number(_, _)) => "number"
Value(Keyword(True, _)) | Value(Keyword(False, _)) => "boolean"
Value(Keyword(Null, _)) => "null"
Value(String(_, _)) => "string"
Value(TextBlock(_, _)) => "textBlock"
Value(ShapeId(_, _)) => "shapeId"
Comment(c) =>
match c.kind {
Line => "line"
Doc => "doc"
}
_ => self.category()
}
}
///|
/// `span` widened to start at the first doc comment when that comes first.
fn with_docs(
span : @span.Span,
docs : ArrayView[@ast.DocComment],
) -> @span.Span {
if docs.length() > 0 && docs[0].span.start < span.start {
docs[0].span.start.to(span.end())
} else {
span
}
}
///|
/// The node's source span, widened over what the AST span leaves out, so a
/// node's span covers all of its children. Doc comments are trivia, so the
/// parser's span of a shape, member, enum member or inline shape starts
/// after them; here it starts at the first doc comment. An operation
/// property covers the docs of its inline shape, and the file covers every
/// comment. Every other node has its AST span.
pub fn NodeRef::span(self : NodeRef) -> @span.Span {
match self {
File(f, comments) => {
let mut s = f.span
for c in comments {
s = s.join(c.span)
}
s
}
Control(c) => c.span
Metadata(m) => m.span
Namespace(n) => n.span
Use(u) => u.span
Shape(s) => with_docs(s.span, s.docs)
Apply(a) => a.span
Inline(i) => with_docs(i.span, i.docs)
Mixins(m) => m.span
Member(m) => with_docs(m.span, m.docs)
EnumMember(m) => with_docs(m.span, m.docs)
Property(Input(Inline(i), s)) | Property(Output(Inline(i), s)) =>
with_docs(s, i.docs)
Property(Input(_, s)) | Property(Output(_, s)) | Property(Errors(_, s)) => s
Trait(t) => t.span
Value(v) => v.span()
Entry(e) => e.span
ShapeIdRef(n) => n.span
Name(n) => n.span
Doc(d) => d.span
Comment(c) => c.span
}
}