///|
/// Parse ignore rules used to filter accepted or experimental API changes.
///
/// Supported rule syntax:
///
/// `ignore KIND NAME [reason...]`
///
/// `KIND` and `NAME` may be `*`; `NAME` may also use a single `*` prefix,
/// suffix, or infix wildcard.
pub fn parse_ignore_rules(text : String) -> IgnoreParseResult {
let rules : Array[ApiIgnoreRule] = []
let errors : Array[String] = []
let mut line_number = 1
for raw_line in split_lines(text[:]) {
parse_ignore_rule_line(raw_line, line_number, rules, errors)
line_number += 1
}
{ rules, errors }
}
///|
/// Filter a report using textual ignore rules.
pub fn filter_report(report : ApiReport, rules_text : String) -> ApiReport {
let parsed = parse_ignore_rules(rules_text)
filter_report_with_rules(report, parsed.rules)
}
///|
/// Filter a report using pre-parsed ignore rules.
pub fn filter_report_with_rules(
report : ApiReport,
rules : Array[ApiIgnoreRule],
) -> ApiReport {
if rules.is_empty() {
return report
}
let changes : Array[ApiChange] = []
for change in report.changes {
if !change_matches_any_ignore_rule(change, rules) {
changes.push(change)
}
}
sort_changes(changes)
{ changes, recommendation: recommended_impact(changes) }
}
///|
fn parse_ignore_rule_line(
raw_line : StringView,
line_number : Int,
rules : Array[ApiIgnoreRule],
errors : Array[String],
) -> Unit {
let line = strip_rule_comment(raw_line).trim()
if line.length() == 0 || line.has_prefix("#") {
return
}
let words = split_rule_words(line)
if words.length() == 0 {
return
}
if words[0] != "ignore" {
errors.push("line \{line_number}: expected 'ignore KIND NAME'")
return
}
if words.length() < 3 {
errors.push("line \{line_number}: ignore rule requires kind and name")
return
}
let item_kind = words[1]
let name = words[2]
if !valid_ignore_kind(item_kind) {
errors.push("line \{line_number}: invalid ignore kind '\{item_kind}'")
return
}
if !valid_ignore_name_pattern(name) {
errors.push("line \{line_number}: invalid ignore name '\{name}'")
return
}
rules.push({ item_kind, name, reason: join_words(words, 3) })
}
///|
fn strip_rule_comment(line : StringView) -> StringView {
let mut in_string = false
let mut escaped = false
let mut i = 0
while i < line.length() {
let ch = line.unsafe_get(i).unsafe_to_char()
if in_string {
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == '"' {
in_string = false
}
} else if ch == '"' {
in_string = true
} else if ch == '#' {
return line.view(start_offset=0, end_offset=i)
} else if ch == '/' &&
i + 1 < line.length() &&
line.unsafe_get(i + 1).unsafe_to_char() == '/' {
return line.view(start_offset=0, end_offset=i)
}
i += 1
}
line
}
///|
fn split_rule_words(line : StringView) -> Array[String] {
let words : Array[String] = []
let mut start = 0
let mut in_word = false
for i in 0.. String {
if start >= words.length() {
return ""
}
let out = StringBuilder()
for i in start.. start {
out.write(" ")
}
out.write(words[i])
}
out.to_string()
}
///|
fn valid_ignore_kind(kind : String) -> Bool {
kind == "*" ||
kind == "fn" ||
kind == "type" ||
kind == "typealias" ||
kind == "struct" ||
kind == "enum" ||
kind == "trait" ||
kind == "impl" ||
kind == "let" ||
kind == "const" ||
kind == "suberror" ||
kind == "using" ||
kind == "field" ||
kind == "constructor" ||
kind == "trait-method" ||
kind == "derive" ||
kind == "unknown"
}
///|
fn valid_ignore_name_pattern(name : String) -> Bool {
if name.length() == 0 {
return false
}
for ch in name {
if is_ascii_space(ch.to_int()) {
return false
}
}
true
}
///|
fn change_matches_any_ignore_rule(
change : ApiChange,
rules : Array[ApiIgnoreRule],
) -> Bool {
for rule in rules {
if change_matches_ignore_rule(change, rule) {
return true
}
}
false
}
///|
fn change_matches_ignore_rule(change : ApiChange, rule : ApiIgnoreRule) -> Bool {
ignore_kind_matches(rule.item_kind, change.item_kind) &&
wildcard_matches(rule.name, change.name)
}
///|
fn ignore_kind_matches(pattern : String, actual : String) -> Bool {
if pattern == "*" || pattern == actual {
return true
}
match actual.find("->") {
Some(index) => {
let left = actual[:index].to_owned()
let right = actual[index + 2:].to_owned()
pattern == left || pattern == right
}
None => false
}
}
///|
fn wildcard_matches(pattern : String, value : String) -> Bool {
if pattern == "*" || pattern == value {
return true
}
if pattern.has_prefix("*") && pattern.has_suffix("*") && pattern.length() > 2 {
return value.contains(pattern[1:pattern.length() - 1])
}
match pattern.find("*") {
None => false
Some(star) =>
if star == 0 && pattern.length() == 1 {
true
} else {
let prefix = pattern[:star]
let suffix = pattern[star + 1:]
value.has_prefix(prefix) && value.has_suffix(suffix)
}
}
}