diff --git a/src/languages/azure_policy/ast/mod.rs b/src/languages/azure_policy/ast/mod.rs new file mode 100644 index 00000000..418adbb2 --- /dev/null +++ b/src/languages/azure_policy/ast/mod.rs @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Span-annotated AST types for Azure Policy rule conditions. +//! +//! AST nodes carry [`Span`] information pointing back into the original JSON +//! source, enabling precise error messages during compilation and validation. +//! +//! The type hierarchy mirrors the Azure Policy JSON structure: +//! - [`PolicyDefinition`] — full policy definition wrapper +//! - [`PolicyRule`] — top-level `{ "if": constraint, "then": { "effect": ... } }` +//! - [`Constraint`] — logical combinators (`allOf`, `anyOf`, `not`) or leaf [`Condition`] +//! - [`Condition`] — `{ lhs, operator, rhs }` triple +//! - [`FieldNode`] / [`FieldKind`] — field reference classification +//! - [`Expr`] — ARM template expression (`"[concat(...)]"`) +//! - [`CountNode`] — `count` with optional `where` clause + +mod value; + +pub use value::*; + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +pub use crate::lexer::Span; + +// ============================================================================ +// Top-level +// ============================================================================ + +/// A parsed Azure Policy rule. +/// +/// Represents the `policyRule` object: +/// ```json +/// { +/// "if": { /* constraint */ }, +/// "then": { "effect": "deny" } +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct PolicyRule { + /// Span covering the entire `policyRule` JSON object. + pub span: Span, + /// The `"if"` condition. + pub condition: Constraint, + /// The `"then"` block containing the effect. + pub then_block: ThenBlock, +} + +/// The `"then"` block of a policy rule. +#[derive(Clone, Debug)] +pub struct ThenBlock { + /// Span covering the `"then"` JSON object. + pub span: Span, + /// The effect (e.g., "deny", "audit", "modify"). + pub effect: EffectNode, + /// Optional details block (for modify/append/deployIfNotExists effects). + pub details: Option, + /// Parsed `existenceCondition` from `details` (for auditIfNotExists / + /// deployIfNotExists). This is extracted from the `details` JSON and + /// parsed as a `Constraint` (same grammar as `policyRule.if`). + pub existence_condition: Option, +} + +/// The `"effect"` value in the then block. +#[derive(Clone, Debug)] +pub struct EffectNode { + /// Span of the effect value string. + pub span: Span, + /// The effect kind. + pub kind: EffectKind, + /// The original effect text as written (preserves casing). + pub raw: String, +} + +/// Known Azure Policy effect types. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EffectKind { + Deny, + Audit, + Append, + AuditIfNotExists, + DeployIfNotExists, + Disabled, + Modify, + DenyAction, + Manual, + /// An effect value that wasn't recognized (may be a parameterized expression). + /// Use [`EffectNode::raw`] to get the original text. + Other, +} + +// ============================================================================ +// Constraints +// ============================================================================ + +/// A logical or leaf constraint node. +/// +/// Maps directly to the Azure Policy JSON structure: +/// - `{ "allOf": [...] }` → `AllOf` +/// - `{ "anyOf": [...] }` → `AnyOf` +/// - `{ "not": {...} }` → `Not` +/// - `{ "field": "...", "equals": "..." }` → `Condition` +#[derive(Clone, Debug)] +pub enum Constraint { + AllOf { + /// Span covering the entire JSON object `{ "allOf": [...] }`. + span: Span, + /// The child constraints. + constraints: Vec, + }, + AnyOf { + /// Span covering the entire JSON object `{ "anyOf": [...] }`. + span: Span, + /// The child constraints. + constraints: Vec, + }, + Not { + /// Span covering the entire JSON object `{ "not": {...} }`. + span: Span, + /// The negated constraint. + constraint: Box, + }, + /// A leaf condition (field/value/count + operator + rhs). + Condition(Box), +} + +// ============================================================================ +// Conditions +// ============================================================================ + +/// A leaf condition: `{ lhs, operator, rhs }`. +/// +/// Example: `{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" }` +#[derive(Clone, Debug)] +pub struct Condition { + /// Span covering the entire condition JSON object. + pub span: Span, + /// The left-hand operand (field, value, or count). + pub lhs: Lhs, + /// The operator (equals, contains, etc.) with its span. + pub operator: OperatorNode, + /// The right-hand value or expression. + pub rhs: ValueOrExpr, +} + +/// The left-hand side of a condition. +#[derive(Clone, Debug)] +pub enum Lhs { + /// `"field": "..."` — a resource field reference. + Field(FieldNode), + /// `"value": ...` — a literal value or expression. + Value { + /// Span of the `"value"` key. + key_span: Span, + /// The value or expression. + value: ValueOrExpr, + }, + /// `"count": { ... }` — a count expression. + Count(CountNode), +} + +// ============================================================================ +// Fields +// ============================================================================ + +/// A field reference with its source span. +#[derive(Clone, Debug)] +pub struct FieldNode { + /// Span of the field string value in the JSON. + pub span: Span, + /// The classified field kind. + pub kind: FieldKind, +} + +/// Classification of a `"field"` string value. +/// +/// Built-in fields are mapped to specific variants; everything else is either +/// an alias or an ARM template expression. +#[derive(Clone, Debug)] +pub enum FieldKind { + /// `"type"` + Type, + /// `"id"` + Id, + /// `"kind"` + Kind, + /// `"name"` + Name, + /// `"location"` + Location, + /// `"fullName"` + FullName, + /// `"tags"` (the entire tags object) + Tags, + /// `"identity.type"` + IdentityType, + /// `"identity."` — any identity sub-field other than `type` + /// (e.g., `"identity.userAssignedIdentities"`, `"identity.principalId"`). + IdentityField(String), + /// `"apiVersion"` + ApiVersion, + /// `"tags.tagName"` or `"tags['tagName']"` + Tag(String), + /// An alias string (e.g., `"Microsoft.Compute/virtualMachines/imagePublisher"`) + Alias(String), + /// An ARM template expression (e.g., `"[concat('Microsoft.Network/', ...)]"`) + Expr(Expr), +} + +// ============================================================================ +// Operators +// ============================================================================ + +/// An operator node with span information. +#[derive(Clone, Debug)] +pub struct OperatorNode { + /// Span of the operator key string in the JSON (e.g., the `"equals"` key). + pub span: Span, + /// The operator kind. + pub kind: OperatorKind, +} + +/// The 19 Azure Policy condition operators. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OperatorKind { + Contains, + ContainsKey, + Equals, + Greater, + GreaterOrEquals, + Exists, + In, + Less, + LessOrEquals, + Like, + Match, + MatchInsensitively, + NotContains, + NotContainsKey, + NotEquals, + NotIn, + NotLike, + NotMatch, + NotMatchInsensitively, +} + +// ============================================================================ +// Policy Definition (full envelope) +// ============================================================================ + +/// A fully parsed Azure Policy definition. +/// +/// Wraps the `properties` section of a policy definition JSON: +/// ```json +/// { +/// "properties": { +/// "displayName": "...", +/// "description": "...", +/// "mode": "All", +/// "parameters": { ... }, +/// "policyRule": { "if": ..., "then": ... } +/// } +/// } +/// ``` +/// +/// Fields that we don't parse into typed members are stored in `extra`. +#[derive(Clone, Debug)] +pub struct PolicyDefinition { + /// Span covering the entire definition JSON object. + pub span: Span, + + /// Optional `displayName`. + pub display_name: Option, + + /// Optional `description`. + pub description: Option, + + /// Optional `mode` (e.g., `"All"`, `"Indexed"`, `"Microsoft.KeyVault.Data"`). + pub mode: Option, + + /// Optional `metadata` (kept as raw JSON). + pub metadata: Option, + + /// Parameter definitions as an ordered list; lookups should match `ParameterDefinition::name`. + pub parameters: Vec, + + /// The parsed `policyRule`. + pub policy_rule: PolicyRule, + + /// Any other top-level fields not handled above (e.g., `id`, `name`, `type`, `policyType`). + pub extra: Vec, +} + +/// A single parameter definition within `properties.parameters`. +/// +/// ```json +/// "paramName": { +/// "type": "String", +/// "defaultValue": "...", +/// "allowedValues": [...], +/// "metadata": { "displayName": "...", "description": "..." } +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct ParameterDefinition { + /// Span covering this parameter's JSON object. + pub span: Span, + + /// The parameter name (the key in the `parameters` object). + pub name: String, + + /// Span of the parameter name key. + pub name_span: Span, + + /// The `type` field (e.g., `"String"`, `"Integer"`, `"Boolean"`, `"Array"`, `"Object"`). + pub param_type: Option, + + /// Optional default value. + pub default_value: Option, + + /// Optional list of allowed values. + pub allowed_values: Option>, + + /// Optional metadata (kept as raw JSON). + pub metadata: Option, + + /// Any extra fields not handled above. + pub extra: Vec, +} diff --git a/src/languages/azure_policy/ast/value.rs b/src/languages/azure_policy/ast/value.rs new file mode 100644 index 00000000..194fc719 --- /dev/null +++ b/src/languages/azure_policy/ast/value.rs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! JSON value types, ARM template expressions, and count nodes. + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::lexer::Span; + +use super::{Constraint, FieldNode}; + +// ============================================================================ +// Values and Expressions +// ============================================================================ + +/// A value that may be a literal JSON value or an ARM template expression. +/// +/// Strings of the form `"[expr]"` are parsed as expressions; everything else +/// remains a plain JSON value. +#[derive(Clone, Debug)] +pub enum ValueOrExpr { + /// A plain JSON value. + Value(JsonValue), + /// An ARM template expression parsed from a `"[...]"` string. + Expr { + /// Span of the entire string (including the `[` and `]`). + span: Span, + /// The raw string content (between the quotes, including `[` and `]`). + raw: String, + /// The parsed expression AST. + expr: Expr, + }, +} + +/// A JSON value with span information. +/// +/// This is our own representation (not `serde_json::Value`) because we need +/// exact source spans for every token. +#[derive(Clone, Debug)] +pub enum JsonValue { + /// JSON `null`. + Null(Span), + /// JSON boolean. + Bool(Span, bool), + /// JSON number (stored as raw string for lossless round-trip). + Number(Span, String), + /// JSON string. + Str(Span, String), + /// JSON array. + Array(Span, Vec), + /// JSON object. + Object(Span, Vec), +} + +/// A key-value entry in a JSON object. +#[derive(Clone, Debug)] +pub struct ObjectEntry { + /// Span of the key string. + pub key_span: Span, + /// The key text. + pub key: String, + /// The value. + pub value: JsonValue, +} + +impl JsonValue { + /// Returns the span of this JSON value. + pub const fn span(&self) -> &Span { + match *self { + Self::Null(ref s) + | Self::Bool(ref s, _) + | Self::Number(ref s, _) + | Self::Str(ref s, _) + | Self::Array(ref s, _) + | Self::Object(ref s, _) => s, + } + } +} + +// ============================================================================ +// ARM Template Expressions +// ============================================================================ + +/// An ARM template expression AST node. +/// +/// Represents expressions inside `"[...]"` strings, following the grammar: +/// ```text +/// expr ::= IDENT +/// | NUMBER +/// | STRING +/// | BOOL +/// | expr '.' IDENT +/// | expr '(' args ')' +/// | expr '[' expr ']' +/// ``` +#[derive(Clone, Debug)] +pub enum Expr { + /// A literal value (number, string, or bool). + Literal { span: Span, value: ExprLiteral }, + /// An identifier (e.g., function name or parameter reference). + Ident { span: Span, name: String }, + /// A function call: `func(arg1, arg2, ...)`. + Call { + span: Span, + func: Box, + args: Vec, + }, + /// A dot access: `expr.field`. + Dot { + span: Span, + object: Box, + field_span: Span, + field: String, + }, + /// An index access: `expr[index]`. + Index { + span: Span, + object: Box, + index: Box, + }, +} + +impl Expr { + /// Returns a reference to this expression's span. + pub const fn span(&self) -> &Span { + match *self { + Self::Literal { ref span, .. } + | Self::Ident { ref span, .. } + | Self::Call { ref span, .. } + | Self::Dot { ref span, .. } + | Self::Index { ref span, .. } => span, + } + } +} + +/// A literal value inside an ARM template expression. +#[derive(Clone, Debug, PartialEq)] +pub enum ExprLiteral { + /// Numeric value (raw string). + Number(String), + /// String value. + String(String), + /// Boolean value (from string `"true"`/`"false"` in expression context). + Bool(bool), +} + +// ============================================================================ +// Count +// ============================================================================ + +/// A `"count"` expression node. +/// +/// Two forms: +/// - **Field count**: `{ "count": { "field": "alias[*]", "where": {...} } }` +/// - **Value count**: `{ "count": { "value": [...], "name": "x", "where": {...} } }` +#[derive(Clone, Debug)] +pub enum CountNode { + /// Count over an alias array field. + Field { + /// Span covering the `"count"` value object. + span: Span, + /// The field being counted (must be an alias or template expression). + field: FieldNode, + /// Optional filter constraint. + where_: Option>, + }, + /// Count over an inline value. + Value { + /// Span covering the `"count"` value object. + span: Span, + /// The JSON value or expression being counted (typically an array). + value: ValueOrExpr, + /// Optional iteration variable name. + name: Option, + /// Optional filter constraint. + where_: Option>, + }, +} + +/// A `"name"` entry in a count-value block. +#[derive(Clone, Debug)] +pub struct NameNode { + /// Span of the name string value. + pub span: Span, + /// The name text. + pub name: String, +} diff --git a/src/languages/azure_policy/mod.rs b/src/languages/azure_policy/mod.rs index f2256fa5..bc64f71c 100644 --- a/src/languages/azure_policy/mod.rs +++ b/src/languages/azure_policy/mod.rs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Azure Policy language support. +//! Azure Policy language support: AST types, aliases, and string utilities. #[allow(clippy::pattern_type_mismatch)] pub mod aliases; +pub mod ast; pub mod strings;