// The bidirectional-control-character lint.
//
// Ported from `lint_confusable` in wax/src/lib-wax/typing.ml.
//
// A whole-module walk over every STRING, wherever one can appear: an instruction
// literal, a data segment, an import's module name, a conditional's operand, an
// attribute's value. The point of the lint is that these characters are
// invisible, so there is no shape to look for -- only every place a string can
// hide.
///|
/// Report the first bidirectional control character in a byte string.
fn check_bytes(
diagnostics : @diagnostic.Context,
location : @basic.Location,
b : Bytes,
) -> Unit {
if @unicode.first_confusable(b) is Some(cp) {
confusable_unicode(diagnostics, location, cp)
}
}
///|
/// Likewise for a conditional's string operands, however deeply nested.
fn check_cond(diagnostics : @diagnostic.Context, c : @wasm_types.Cond) -> Unit {
match c {
Str(s) => check_bytes(diagnostics, s.info, s.desc)
Var(_) | Version(_, _, _) => ()
And(l) | Or(l) =>
for x in l {
check_cond(diagnostics, x)
}
Not(x) => check_cond(diagnostics, x)
Cmp(_, a, b) => {
check_cond(diagnostics, a)
check_cond(diagnostics, b)
}
}
}
///|
/// Every string inside one instruction and everything under it.
fn check_instrs(
diagnostics : @diagnostic.Context,
instrs : Array[@ast.Instr[@basic.Location]],
) -> Unit {
for i in instrs {
i.iter_instr(node => {
match node.desc {
Str(_, s) => check_bytes(diagnostics, node.info, s)
IfAnnotation(cond~, ..) => check_cond(diagnostics, cond)
_ => ()
}
})
}
}
///|
/// An attribute's value and its guard both hold source the reader sees.
fn check_attrs(
diagnostics : @diagnostic.Context,
attrs : Array[@ast.Attribute],
) -> Unit {
for a in attrs {
if a.attr_value is Some(v) {
check_instrs(diagnostics, [v])
}
if a.attr_guard is Some(g) {
check_cond(diagnostics, g.desc)
}
}
}
///|
/// A data segment's contents, which are strings by construction.
fn check_data(
diagnostics : @diagnostic.Context,
location : @basic.Location,
init : Array[@ast.DataElem],
) -> Unit {
for e in init {
match e {
Str(s) => check_bytes(diagnostics, location, s)
// A run's elements are numeric literals written as text, so they hold no
// string a reader could be misled by.
Run(_, _) => ()
V128Run(_) => ()
}
}
}
///|
/// Walk a module reporting every string that hides one of these characters.
fn lint_confusable(
diagnostics : @diagnostic.Context,
fields : @ast.Module[@basic.Location],
) -> Unit {
for field in fields {
let location = field.info
match field.desc {
Type(_) => ()
Func(body~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
check_instrs(diagnostics, body.1)
}
Global(def~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
check_instrs(diagnostics, [def])
}
Tag(attributes~, ..) => check_attrs(diagnostics, attributes)
Memory(data~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
for m in data {
check_data(diagnostics, location, m.init)
}
}
Data(init~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
check_data(diagnostics, location, init)
}
Table(init~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
if init is Some(x) {
check_instrs(diagnostics, [x])
}
}
Elem(init~, attributes~, ..) => {
check_attrs(diagnostics, attributes)
check_instrs(diagnostics, init)
}
Import(module_~, decl~) => {
check_bytes(diagnostics, module_.info, module_.desc)
check_attrs(diagnostics, decl.desc.attributes)
}
ImportGroup(module_~, decls~) => {
check_bytes(diagnostics, module_.info, module_.desc)
for d in decls {
check_attrs(diagnostics, d.desc.attributes)
}
}
ModuleAnnotation(attrs) => check_attrs(diagnostics, attrs)
Conditional(cond~, then_fields~, else_fields~) => {
check_cond(diagnostics, cond)
lint_confusable(diagnostics, then_fields.desc)
if else_fields is Some(f) {
lint_confusable(diagnostics, f.desc)
}
}
}
}
}