Source code for nada_dsl.audit.strict

"""
Static analysis submodule that defines the highly limited "strict"
subset of the Nada DSL syntax.
"""
# pylint: disable=wildcard-import,unused-wildcard-import,invalid-name
from __future__ import annotations
from typing import Callable
import ast
import richreports

try:
    from nada_dsl.audit.abstract import *
    from nada_dsl.audit.common import *
    from nada_dsl.audit.report import parse, enrich_fromaudits
except: # pylint: disable=bare-except # For Nada DSL Sandbox support.
    from abstract import *
    from common import *
    from report import parse, enrich_fromaudits

def _rules_restrictions_descendants(a):
    """
    Remove all rule attributes that are redundant because they occur in a
    an ancestor.
    """
    for a_ in ast.walk(a):
        if not isinstance(a_, (ast.Assign, ast.Assign)):
            if isinstance(audits(a_, 'rules'), (SyntaxRestriction, RuleInAncestor)):
                for a__ in ast.walk(a_):
                    if a__ != a_:
                        audits(a__, 'rules', RuleInAncestor())
                        audits(a__, 'types', delete=True)

[docs] def rules(a): """ Mark all :obj:`ast` nodes as prohibited (as a starting point for a strict static analysis). """ if isinstance(a, ast.Module): for a_ in ast.walk(a): audits( a_, 'rules', SyntaxRestriction('use of this syntax is prohibited in strict mode') )
def _types_base(t): """ Return a boolean value indicating whether the supplied type is a base type. """ return t in ( bool, int, str, Integer, PublicInteger, SecretInteger, Boolean, PublicBoolean, SecretBoolean ) def _types_list_monomorphic(t): """ Return a boolean value indicating whether the supplied type represents a monomorphic list type (*i.e.*, a list type wherein the types of the items are fully specified). """ if t.__name__ == 'list' and hasattr(t, '__args__') and len(t.__args__) == 1: return _types_list_monomorphic(t.__args__[0]) if _types_base(t): return True return False def _types_list_monomorphic_depth(t): """ Return an integer representing the depth of the monomorphic list type. """ if t.__name__ == 'list' and hasattr(t, '__args__') and len(t.__args__) == 1: return 1 + _types_list_monomorphic_depth(t.__args__[0]) return 0 def _types_monomorphic(t): """ Return a boolean value indicating whether the supplied type represents a monomorphic type permitted by this static analysis submodule. """ return _types_base(t) or _types_list_monomorphic(t) def _types_binop_mult_add_sub(t_l, t_r): """ Determine the result type for multiplication and addition operations involving Nada DSL integers. """ t = TypeErrorRoot('arguments must have integer types') if (t_l, t_r) == (Integer, Integer): t = Integer elif (t_l, t_r) == (Integer, PublicInteger): t = PublicInteger elif (t_l, t_r) == (PublicInteger, Integer): t = PublicInteger elif (t_l, t_r) == (Integer, SecretInteger): t = SecretInteger elif (t_l, t_r) == (SecretInteger, Integer): t = SecretInteger elif (t_l, t_r) == (PublicInteger, PublicInteger): t = PublicInteger elif (t_l, t_r) == (PublicInteger, SecretInteger): t = SecretInteger elif (t_l, t_r) == (SecretInteger, PublicInteger): t = SecretInteger elif (t_l, t_r) == (SecretInteger, SecretInteger): t = SecretInteger if isinstance(t_l, TypeError): t = typeerror_demote(t_l) elif isinstance(t_r, TypeError): t = typeerror_demote(t_r) return t def _types_compare(t_l, t_r): """ Determine the result type for comparison operations involving Nada DSL integers. """ t = TypeErrorRoot('arguments must have integer types') if (t_l, t_r) == (Integer, Integer): t = Boolean elif (t_l, t_r) == (Integer, PublicInteger): t = PublicBoolean elif (t_l, t_r) == (PublicInteger, Integer): t = PublicBoolean elif (t_l, t_r) == (Integer, SecretInteger): t = SecretBoolean elif (t_l, t_r) == (SecretInteger, Integer): t = SecretBoolean elif (t_l, t_r) == (PublicInteger, PublicInteger): t = PublicBoolean elif (t_l, t_r) == (PublicInteger, SecretInteger): t = SecretBoolean elif (t_l, t_r) == (SecretInteger, PublicInteger): t = SecretBoolean elif (t_l, t_r) == (SecretInteger, SecretInteger): t = SecretBoolean if isinstance(t_l, TypeError): t = typeerror_demote(t_l) elif isinstance(t_r, TypeError): t = typeerror_demote(t_r) return t
[docs] def types(a, env=None, func=False): """ Infer types of :obj:`ast` where possible, adding the type (or error) information in node attributes (and removing syntax restriction attributes as appropriate). """ # pylint: disable=too-many-statements,too-many-return-statements # pylint: disable=too-many-nested-blocks,too-many-branches # pylint: disable=too-many-locals env = {} if env is None else env # Handle cases in which the input node is a statement. if isinstance(a, ast.Module): rules_no_restriction(a) for a_ in a.body: env = types(a_, env, func) # Remove syntax restriction attributes that appear for descendants # of nodes that already have such attributes. _rules_restrictions_descendants(a) return env if isinstance(a, ast.ImportFrom): if ( a.module == 'nada_dsl' and len(a.names) == 1 and a.names[0].name == '*' and a.names[0].asname is None and a.level == 0 ): rules_no_restriction(a, recursive=True) return env if isinstance(a, ast.FunctionDef): if not func: if a.name == 'nada_main': rules_no_restriction(a) rules_no_restriction(a.args) # Create a local copy of the environment. Only the # original environment passed to this invocation # is returned. env_ = dict(env) for a_ in a.body: env_ = types(a_, env_, func=True) else: rules_no_restriction(a) rules_no_restriction(a.args) t_ret = eval(ast.unparse(a.returns)) # pylint: disable=eval-used if _types_monomorphic(t_ret): rules_no_restriction(a.returns) env_ = dict(env) ts = [] for arg in a.args.args: var = arg.arg t_var = eval(ast.unparse(arg.annotation)) # pylint: disable=eval-used if _types_monomorphic(t_var): rules_no_restriction(arg) rules_no_restriction(arg.annotation, recursive=True) env_[var] = t_var ts.append(t_var) for a_ in a.body: env_ = types(a_, env_, func=True) env[a.name] = Callable[ts, t_ret] return env if isinstance(a, ast.Assign): if len(a.targets) == 1: target = a.targets[0] if isinstance(target, ast.Name): rules_no_restriction(a) rules_no_restriction(target) types(a.value, env, func) t = audits(a.value, 'types') if t == list and not _types_list_monomorphic(t): t = TypeErrorRoot( 'assignment of list value with underspecified ' + 'type requires fully specified type annotation' ) audits(a, 'types', t) elif t is not None: audits(a, 'types', typeerror_demote(t)) audits(target, 'types', TypeInParent()) if not isinstance(t, TypeError): if isinstance(target, ast.Name): var = target.id env[var] = t elif isinstance(target, ast.Subscript): rules_no_restriction(a) types(a.value, env, func) t = audits(a.value, 'types') target_ = target invalid_index = False depth = 0 while isinstance(target_, ast.Subscript): depth += 1 rules_no_restriction(target_) types(target_.slice, env, func) t_s = audits(target_.slice, 'types') if t_s != int: invalid_index = True break if isinstance(target_.value, (ast.Name, ast.Subscript)): target_ = target_.value if invalid_index: audits(a, 'types', TypeErrorRoot('indices must be integers')) if isinstance(target_, ast.Name): rules_no_restriction(target) rules_no_restriction(target_) if target_.id in env: t_b = env[target_.id] if _types_list_monomorphic_depth(t_b) < depth: audits(a, 'types', TypeErrorRoot('target has incompatible type')) else: audits(a, 'types', typeerror_demote(t)) else: audits(a, 'types', TypeErrorRoot('unbound variable: ' + target_.id)) return env if isinstance(a, ast.AnnAssign): if isinstance(a.target, ast.Name): rules_no_restriction(a) rules_no_restriction(a.target) types(a.value, env, func) t = audits(a.value, 'types') try: t_a = eval(ast.unparse(a.annotation)) # pylint: disable=eval-used rules_no_restriction(a.annotation, recursive=True) if not _types_list_monomorphic(t_a): audits( a.annotation, 'types', TypeErrorRoot( 'assignment of list value requires fully specified type annotation' ) ) except: # pylint: disable=bare-except t_a = TypeErrorRoot('invalid type annotation') if isinstance(t, TypeError): audits(a, 'types', t) audits(a.target, 'types', t) else: t_u = unify(t_a, t) if t_u is None: t = TypeErrorRoot('value type cannot be reconciled with type annotation') audits(a, 'types', t) audits(a.target, 'types', t) else: t = t_u audits(a, 'types', t) audits(a.target, 'types', TypeInParent()) var = a.target.id env[var] = t return env if isinstance(a, ast.Return): if func: rules_no_restriction(a) types(a.value, env, func) return env if isinstance(a, ast.For): rules_no_restriction(a) types(a.iter, env, func) t_i = audits(a.iter, 'types') if isinstance(a.target, ast.Name): rules_no_restriction(a.target) var = a.target.id audits(a.target, 'types', int) if isinstance(t_i, TypeError): pass # Allow the error to pass through. elif t_i == range: env[var] = int for a_ in a.body: env = types(a_, env, func) else: audits(a.iter, 'types', TypeErrorRoot('iterable must be a range')) return env if isinstance(a, ast.Expr): types(a.value, env, func) if not isinstance(audits(a.value, 'rules'), SyntaxRestriction): rules_no_restriction(a) return env # Handle cases in which the input node is an expression. audits(a, 'types', TypeError('type cannot be determined')) if isinstance(a, ast.ListComp): rules_no_restriction(a) ts = {} for comprehension in a.generators: rules_no_restriction(comprehension) types(comprehension.iter, env, func) t_c = audits(comprehension.iter, 'types') if isinstance(comprehension.target, ast.Name): rules_no_restriction(comprehension.target) var = comprehension.target.id t = int if isinstance(t_c, TypeError): pass # Allow the error to pass through. elif t_c == range: rules_no_restriction(comprehension.iter) ts[var] = t else: t = TypeErrorRoot('iterable must be a range value') audits(comprehension.iter, 'types', t) t = typeerror_demote(t) audits(comprehension.target, 'types', t) env_ = dict(env) for (var, t_) in ts.items(): env_[var] = t_ types(a.elt, env_, func) t_e = audits(a.elt, 'types') if t_e is not None and not isinstance(t_e, TypeError): audits(a, 'types', list[t_e]) elif isinstance(a, ast.Call): ats = [] kts = [] for a_ in a.args: types(a_, env, func) ats.append(audits(a_, 'types')) for a_ in a.keywords: types(a_.value, env, func) kts.append(audits(a_.value, 'types')) if isinstance(a.func, ast.Attribute): types(a.func.value, env, func) t_v = audits(a.func.value, 'types') if a.func.attr == 'if_else': if len(a.args) == 2: ts = ats rules_no_restriction(a) rules_no_restriction(a.func) if t_v == Boolean: if ( ts[0] in (Integer, PublicInteger, SecretInteger) and ts[1] in (Integer, PublicInteger, SecretInteger) ): t = max(ts) else: t = TypeErrorRoot('branches must have the same integer type') elif t_v == PublicBoolean: if ( ts[0] in (Integer, PublicInteger, SecretInteger) and ts[1] in (Integer, PublicInteger, SecretInteger) ): t = max(ts) else: t = TypeErrorRoot('branches must have the same integer type') elif t_v == SecretBoolean: if ( ts[0] in (Integer, PublicInteger, SecretInteger) and ts[1] in (Integer, PublicInteger, SecretInteger) ): t = max(ts) else: t = TypeErrorRoot('branches must have the same integer type') else: t = TypeErrorRoot('condition must have a boolean type') audits(a, 'types', t) elif a.func.attr == 'append': if len(a.args) == 1: rules_no_restriction(a) rules_no_restriction(a.func) t_i = ats[0] if unify(t_v, list[t_i]): audits(a, 'types', type(None)) else: audits( a, 'types', TypeErrorRoot('item type does not match list type') ) elif isinstance(a.func, ast.Name): if a.func.id == 'Party': rules_no_restriction(a) rules_no_restriction(a.func) t = TypeError('party requires name parameter (a string)') if ( len(a.args) == 0 and len(a.keywords) == 1 and a.keywords[0].arg == 'name' ): rules_no_restriction(a.keywords[0]) if audits(a.keywords[0].value, 'types') == str: t = Party elif ( len(a.args) == 1 and len(a.keywords) == 0 ): rules_no_restriction(a.args[0]) if audits(a.args[0], 'types') == str: t = Party audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'Input': rules_no_restriction(a) rules_no_restriction(a.func) t = TypeError('input requires name parameter (a string) and party parameter') if ( len(a.args) == 2 and len(a.keywords) == 0 ): rules_no_restriction(a.args[0]) rules_no_restriction(a.args[1]) if ( audits(a.args[0], 'types') == str and audits(a.args[1], 'types') == Party ): t = Input if ( len(a.args) == 1 and len(a.keywords) == 1 and a.keywords[0].arg == 'party' ): rules_no_restriction(a.args[0]) rules_no_restriction(a.keywords[0]) if ( audits(a.args[0], 'types') == str and audits(a.keywords[0].value, 'types') == Party ): t = Input if ( len(a.args) == 0 and len(a.keywords) == 2 and a.keywords[0].arg == 'name' and a.keywords[1].arg == 'party' ): rules_no_restriction(a.keywords[0]) rules_no_restriction(a.keywords[1]) if ( audits(a.keywords[0].value, 'types') == str and audits(a.keywords[1].value, 'types') == Party ): t = Input if ( len(a.args) == 0 and len(a.keywords) == 2 and a.keywords[1].arg == 'name' and a.keywords[0].arg == 'party' ): rules_no_restriction(a.keywords[0]) rules_no_restriction(a.keywords[1]) if ( audits(a.keywords[1].value, 'types') == str and audits(a.keywords[0].value, 'types') == Party ): t = Input audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'Output': rules_no_restriction(a) rules_no_restriction(a.func) t = TypeError( 'output requires value parameter, name parameter (a string), ' + 'and party parameter' ) kwargs = {kw.arg: kw.value for kw in a.keywords} if len(a.args) == 0 and len(a.keywords) == 3: if set(kwargs.keys()) == {'value', 'name', 'party'}: for kw in a.keywords: rules_no_restriction(kw) if ( ( audits(kwargs['value'], 'types') in (SecretInteger, PublicInteger) ) and audits(kwargs['name'], 'types') == str and audits(kwargs['party'], 'types') == Party ): t = Output elif len(a.args) == 1 and len(a.keywords) == 2: if set(kwargs.keys()) == {'name', 'party'}: for arg in a.args: rules_no_restriction(arg) for kw in a.keywords: rules_no_restriction(kw) if ( ( audits(a.args[0], 'types') in (SecretInteger, PublicInteger) ) and audits(kwargs['name'], 'types') == str and audits(kwargs['party'], 'types') == Party ): t = Output elif len(a.args) == 2 and len(a.keywords) == 1: if set(kwargs.keys()) == {'party'}: for arg in a.args: rules_no_restriction(arg) for kw in a.keywords: rules_no_restriction(kw) if ( ( audits(a.args[0], 'types') in (SecretInteger, PublicInteger) ) and audits(a.args[1], 'types') == str and audits(kwargs['party'], 'types') == Party ): t = Output elif len(a.args) == 3 and len(a.keywords) == 0: for arg in a.args: rules_no_restriction(arg) for kw in a.keywords: rules_no_restriction(kw) if ( ( audits(a.args[0], 'types') in (SecretInteger, PublicInteger) ) and audits(a.args[1], 'types') == str and audits(a.args[2], 'types') == Party ): t = Output audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'Integer': rules_no_restriction(a) rules_no_restriction(a.func) t = Integer if ( len(a.args) != 1 or audits(a.args[0], 'types') != int ): t = TypeError('expecting single argument (an integer)') audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'PublicInteger': rules_no_restriction(a) rules_no_restriction(a.func) t = PublicInteger if ( len(a.args) != 1 or audits(a.args[0], 'types') != Input ): t = TypeError('expecting single argument (an input object)') audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'SecretInteger': rules_no_restriction(a) rules_no_restriction(a.func) t = SecretInteger if ( len(a.args) != 1 or audits(a.args[0], 'types') != Input ): t = TypeError('expecting single argument (an input object)') audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'range': rules_no_restriction(a) rules_no_restriction(a.func) t = range if ( len(a.args) != 1 or audits(a.args[0], 'types') != int ): t = TypeErrorRoot('expecting single integer argument') if isinstance(audits(a.args[0], 'types'), TypeError): t = typeerror_demote(t) audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'str': rules_no_restriction(a) rules_no_restriction(a.func) t = str if ( len(a.args) != 1 or audits(a.args[0], 'types') != int ): t = TypeError('expecting single integer argument') audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id == 'sum': rules_no_restriction(a) rules_no_restriction(a.func) t = SecretInteger if ( len(a.args) != 1 or audits(a.args[0], 'types') != list[SecretInteger] ): t = TypeError('expecting argument of type list[SecretInteger]') audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif a.func.id in env: rules_no_restriction(a) rules_no_restriction(a.func) t_f = env[a.func.id] ts = t_f.__args__[:-1] t = TypeErrorRoot('function arguments do not match function type') if len(ats) == len(ts): if all(t_a == t for (t_a, t) in zip(ats, ts)): t = t_f.__args__[-1] audits(a, 'types', t) audits(a.func, 'types', TypeInParent()) elif isinstance(a, ast.Subscript): rules_no_restriction(a) types(a.value, env, func) types(a.slice, env, func) t_v = audits(a.value, 'types') t_s = audits(a.slice, 'types') if (not isinstance(t_v, TypeError)) and (not isinstance(t_s, TypeError)): t = TypeErrorRoot('expecting list value and integer index') if t_v.__name__ == 'list' and t_s == int: if hasattr(t_v, '__args__') and len(t_v.__args__) == 1: t = t_v.__args__[0] audits(a, 'types', t) elif isinstance(a, ast.List): rules_no_restriction(a) for a_ in a.elts: types(a_, env, func) ts = [audits(a_, 'types') for a_ in a.elts] t = TypeError('lists must contain elements that are all of the same type') if len(set(ts)) == 0: t = list elif len(set(ts)) == 1: t = ts[0] if isinstance(t, TypeError): t = typeerror_demote(t) else: t = list[t] audits(a, 'types', t) elif isinstance(a, ast.BoolOp): rules_no_restriction(a) ts = [] for a_ in a.values: types(a_, env, func) ts.append(audits(a_, 'types')) t = TypeErrorRoot('arguments must be boolean values') if all(t == bool for t in ts): t = bool elif any(isinstance(t, TypeError) for t in ts): t = typeerror_demote(t) audits(a, 'types', t) elif isinstance(a, ast.BinOp): types(a.left, env, func) types(a.right, env, func) t_l = audits(a.left, 'types') t_r = audits(a.right, 'types') if isinstance(a.op, ast.Add): rules_no_restriction(a) t = TypeError('unsupported operand types') if t_l == int and t_r == int: t = int elif t_l == str and t_r == str: t = str else: t = _types_binop_mult_add_sub(t_l, t_r) audits(a, 'types', t) elif isinstance(a.op, ast.Sub): rules_no_restriction(a) t = TypeError('unsupported operand types') if t_l == int and t_r == int: t = int else: t = _types_binop_mult_add_sub(t_l, t_r) audits(a, 'types', t) elif isinstance(a.op, ast.Mult): rules_no_restriction(a) if t_l == int and t_r == int: t = int else: t = _types_binop_mult_add_sub(t_l, t_r) audits(a, 'types', t) elif isinstance(a, ast.Compare): if len(a.comparators) != 1: audits( a, 'rules', SyntaxRestriction('chained comparisons are prohibited in strict mode') ) else: op = a.ops[0] rules_no_restriction(a) types(a.left, env, func) types(a.comparators[0], env, func) t_l = audits(a.left, 'types') t_r = audits(a.comparators[0], 'types') if isinstance(op, (ast.Eq, ast.NotEq)): if t_l == bool and t_r == bool: t = bool elif t_l == int and t_r == int: t = bool elif t_l == str and t_r == str: t = bool else: t = _types_compare(t_l, t_r) elif isinstance(op, (ast.Lt, ast.LtE, ast.Gt, ast.GtE)): if t_l == int and t_r == int: t = bool else: t = _types_compare(t_l, t_r) audits(a, 'types', t) elif isinstance(a, ast.UnaryOp): types(a.operand, env, func) t = audits(a.operand, 'types') if isinstance(a.op, (ast.UAdd, ast.USub)): rules_no_restriction(a) if isinstance(t, TypeError): t = typeerror_demote(t) elif t in (int, Integer, PublicInteger, SecretInteger): pass else: t = TypeErrorRoot('argument must have an integer type') audits(a, 'types', t) elif isinstance(a.op, ast.Not): rules_no_restriction(a) if isinstance(t, TypeError): t = typeerror_demote(t) elif t == bool: pass else: t = TypeErrorRoot('argument must be a boolean value') audits(a, 'types', t) elif isinstance(a, ast.Name): rules_no_restriction(a) var = a.id audits( a, 'types', env[var] if var in env else TypeError("name '" + var + "' is not defined") ) elif isinstance(a, ast.Constant): if a.value in (False, True) and str(a.value) in ('False', 'True'): rules_no_restriction(a) audits(a, 'types', bool) elif isinstance(a.value, int): rules_no_restriction(a) audits(a, 'types', int) elif isinstance(a.value, str): rules_no_restriction(a) audits(a, 'types', str) return env # Always return the environment.
[docs] def strict(source: str) -> richreports.report: """ Take a Python source file representing a Nada DSL program, statically analyze it (against the "strict" Nada DSL subset), and generate an interactive HTML report detailing the results. """ source = source.strip() (atok, skips) = parse(source) root = atok.tree # Perform the static analyses. rules(root) types(root) # Perform the abstract execution. #root.body.append(ast.Expr(ast.Call(ast.Name('nada_main', ast.Load()), [], []))) #ast.fix_missing_locations(root) #exec(compile(root, path, 'exec')) # Add the results of the analyses to the report and ensure each line is # wrapped as an HTML element. report = richreports.report(source, line=1, column=0) enrich_fromaudits(report, atok) for (i, line) in enumerate(report.lines): if i in skips: report.enrich( (i + 1, 0), (i + 1, len(line) - 1), '<span class="rules-SyntaxError">', '</span>', skip_whitespace=True ) report.enrich( (i + 1, 0), (i + 1, len(line) - 1), '<span class="detail" data-detail="SyntaxError">', '</span>', skip_whitespace=True ) report.enrich((i + 1, 0), (i + 1, len(line)), '<div>', '</div>') return report