///|
pub(all) struct VariableUse {
name : String
operator : String
prefix : Int32?
explode : Bool
expression_index : Int32
span : Span
} derive(Debug)
///|
/// Ordered by first occurrence. All uses are retained, not just a summary.
pub(all) struct VariableProfile {
name : String
occurrences : Int32
first_span : Span
last_span : Span
scalar_only : Bool
uses_explode : Bool
uses_reserved : Bool
uses_query : Bool
uses_fragment : Bool
maximum_prefix : Int32?
operators : Array[String]
prefixes : Array[Int32]
uses : Array[VariableUse]
} derive(Debug)
///|
fn Template::variable_uses(self : Template) -> Array[VariableUse] {
let result : Array[VariableUse] = []
let mut expression_index = 0
for segment in self.segments {
if segment is Expression(op, specs) {
for spec in specs {
result.push({
name: spec.name,
operator: if op == ' ' {
""
} else {
op.to_string()
},
prefix: match spec.modifier {
Prefix(n) => Some(n)
_ => None
},
explode: spec.modifier is Explode,
expression_index,
span: spec.span,
})
}
expression_index = expression_index + 1
}
}
result
}
///|
fn profile(name : String, uses : Array[VariableUse]) -> VariableProfile {
let operators : Array[String] = []
let prefixes : Array[Int32] = []
let seen_prefixes : Map[Int32, Bool] = Map([])
let mut maximum_prefix = None
let mut explode = false
for occurrence in uses {
if !operators.contains(occurrence.operator) {
operators.push(occurrence.operator)
}
explode = explode || occurrence.explode
if occurrence.prefix is Some(n) {
if !seen_prefixes.contains(n) {
prefixes.push(n)
seen_prefixes[n] = true
}
maximum_prefix = Some(
match maximum_prefix {
Some(old) => Int32::max(old, n)
None => n
},
)
}
}
{
name,
occurrences: uses.length(),
first_span: uses[0].span,
last_span: uses[uses.length() - 1].span,
scalar_only: !prefixes.is_empty(),
uses_explode: explode,
uses_reserved: operators.contains("+") || operators.contains("#"),
uses_query: operators.contains("?") || operators.contains("&"),
uses_fragment: operators.contains("#"),
maximum_prefix,
operators,
prefixes,
uses,
}
}
///|
fn profiles_from_uses(uses : Array[VariableUse]) -> Array[VariableProfile] {
let names : Array[String] = []
let groups : Map[String, Array[VariableUse]] = Map([])
for occurrence in uses {
match groups.get(occurrence.name) {
Some(group) => group.push(occurrence)
None => {
names.push(occurrence.name)
groups[occurrence.name] = [occurrence]
}
}
}
names.map(name => profile(name, groups[name]))
}
///|
pub(all) struct TemplateAnalysis {
expressions : Array[ExpressionInfo]
uses : Array[VariableUse]
profiles : Array[VariableProfile]
max_level : Int32
has_reserved_expansion : Bool
has_fragment : Bool
} derive(Debug)
///|
pub fn Template::analyze(self : Template) -> TemplateAnalysis {
let expressions = self.expressions()
let uses = self.variable_uses()
let mut max_level = 0
let mut reserved = false
let mut fragment = false
for expression in expressions {
max_level = Int32::max(max_level, expression.level)
reserved = reserved ||
expression.operator == "+" ||
expression.operator == "#"
fragment = fragment || expression.operator == "#"
}
{
expressions,
uses,
profiles: profiles_from_uses(uses),
max_level,
has_reserved_expansion: reserved,
has_fragment: fragment,
}
}
///|
pub(all) struct VariableChange {
name : String
before : VariableProfile
after : VariableProfile
operators_changed : Bool
prefixes_changed : Bool
explode_changed : Bool
occurrences_changed : Bool
uses_changed : Bool
} derive(Debug)
///|
/// A review aid, never a claim of business or URL equivalence.
pub(all) struct DetailedContractDiff {
added_variables : Array[String]
removed_variables : Array[String]
became_scalar_only : Array[String]
became_composite_capable : Array[String]
new_repeated_variables : Array[String]
old_level : Int32
new_level : Int32
reserved_added : Bool
fragment_added : Bool
literals_changed : Bool
expression_layout_changed : Bool
variable_changes : Array[VariableChange]
source_changed : Bool
} derive(Debug)
///|
fn same_uses(a : Array[VariableUse], b : Array[VariableUse]) -> Bool {
if a.length() != b.length() {
return false
}
for i, x in a {
let y = b[i]
if x.operator != y.operator ||
x.prefix != y.prefix ||
x.explode != y.explode ||
x.expression_index != y.expression_index {
return false
}
}
true
}
///|
fn same_specs(a : Array[VarSpec], b : Array[VarSpec]) -> Bool {
if a.length() != b.length() {
return false
}
for i, x in a {
let y = b[i]
if x.name != y.name {
return false
}
match (x.modifier, y.modifier) {
(NoModifier, NoModifier) | (Explode, Explode) => ()
(Prefix(x), Prefix(y)) => if x != y { return false }
_ => return false
}
}
true
}
///|
pub fn Template::diff(
self : Template,
newer : Template,
) -> DetailedContractDiff {
let old = self.analyze()
let next = newer.analyze()
let old_profiles : Map[String, VariableProfile] = Map([])
let next_profiles : Map[String, VariableProfile] = Map([])
for p in old.profiles {
old_profiles[p.name] = p
}
for p in next.profiles {
next_profiles[p.name] = p
}
let scalar : Array[String] = []
let composite : Array[String] = []
let repeated : Array[String] = []
let changes : Array[VariableChange] = []
for p in next.profiles {
match old_profiles.get(p.name) {
None => if p.occurrences > 1 { repeated.push(p.name) }
Some(before) => {
if p.scalar_only && !before.scalar_only {
scalar.push(p.name)
}
if !p.scalar_only && before.scalar_only {
composite.push(p.name)
}
if p.occurrences > 1 && before.occurrences <= 1 {
repeated.push(p.name)
}
let uses_changed = !same_uses(before.uses, p.uses)
if uses_changed {
changes.push({
name: p.name,
before,
after: p,
operators_changed: before.uses.map(u => u.operator) !=
p.uses.map(u => u.operator),
prefixes_changed: before.uses.map(u => u.prefix) !=
p.uses.map(u => u.prefix),
explode_changed: before.uses.map(u => u.explode) !=
p.uses.map(u => u.explode),
occurrences_changed: before.occurrences != p.occurrences,
uses_changed,
})
}
}
}
}
let mut literals_changed = false
let mut layout_changed = false
let size = Int32::max(self.segments.length(), newer.segments.length())
for i = 0; i < size; i = i + 1 {
let a = self.segments.get(i)
let b = newer.segments.get(i)
match (a, b) {
(Some(Literal(x, _)), Some(Literal(y, _))) =>
if x != y {
literals_changed = true
}
(Some(Expression(x, xs)), Some(Expression(y, ys))) =>
if x != y || !same_specs(xs, ys) {
layout_changed = true
}
_ => {
if a is Some(Literal(_, _)) || b is Some(Literal(_, _)) {
literals_changed = true
}
if a is Some(Expression(_, _)) || b is Some(Expression(_, _)) {
layout_changed = true
}
}
}
}
{
added_variables: next.profiles
.filter(p => !old_profiles.contains(p.name))
.map(p => p.name),
removed_variables: old.profiles
.filter(p => !next_profiles.contains(p.name))
.map(p => p.name),
became_scalar_only: scalar,
became_composite_capable: composite,
new_repeated_variables: repeated,
old_level: old.max_level,
new_level: next.max_level,
reserved_added: !old.has_reserved_expansion && next.has_reserved_expansion,
fragment_added: !old.has_fragment && next.has_fragment,
literals_changed,
expression_layout_changed: layout_changed,
variable_changes: changes,
source_changed: self.source_text != newer.source_text,
}
}
///|
pub fn DetailedContractDiff::requires_review(
self : DetailedContractDiff,
) -> Bool {
self.source_changed
}
///|
pub extend VariableUse with Debug::{to_repr}
///|
pub extend VariableProfile with Debug::{to_repr}
///|
pub extend TemplateAnalysis with Debug::{to_repr}
///|
pub extend VariableChange with Debug::{to_repr}
///|
pub extend DetailedContractDiff with Debug::{to_repr}