Module refinery.lib.scripts.ps1.analysis.calls
What a call answers where a measured fact covers it.
CALL_FACTS says what Windows PowerShell 5.1 was measured to do with
a call: which arguments the fact covers, whether such a call can still fail, and what it hands back.
A fact holds only where the call still runs what was measured. That is a question about the script
around the call, so it is answered here, and nowhere else, before a fact is read: no write to the
type system may have run before a method call, the name of a command must still denote the command,
and the script must not bind default parameter values, which change what a command does without a
word at the call.
Ps1CallReader answers for one script under one world, and
evaluate() asks it of every call it meets when a caller
hands one in, so that a call is followed wherever it stands: as the receiver of another call, as an
argument, or as the operand of an operator.
Expand source code Browse git
"""
What a call answers where a measured fact covers it.
`refinery.lib.scripts.ps1.data.CALL_FACTS` says what Windows PowerShell 5.1 was measured to do with
a call: which arguments the fact covers, whether such a call can still fail, and what it hands back.
A fact holds only where the call still runs what was measured. That is a question about the script
around the call, so it is answered here, and nowhere else, before a fact is read: no write to the
type system may have run before a method call, the name of a command must still denote the command,
and the script must not bind default parameter values, which change what a command does without a
word at the call.
`Ps1CallReader` answers for one script under one world, and
`refinery.lib.scripts.ps1.analysis.values.evaluate` asks it of every call it meets when a caller
hands one in, so that a call is followed wherever it stands: as the receiver of another call, as an
argument, or as the operand of an operator.
"""
from __future__ import annotations
from weakref import WeakKeyDictionary, ref
from refinery.lib.scripts.ps1.analysis.session import session_of
from refinery.lib.scripts.ps1.analysis.values import (
NOTHING,
NULL,
UNKNOWN,
Ps1Constant,
Ps1Outcome,
Ps1Throws,
Ps1Typed,
evaluate,
throw_join,
type_of,
)
from refinery.lib.scripts.ps1.analysis.worldflow import Ps1WorldReach
from refinery.lib.scripts.ps1.ast import get_command_name
from refinery.lib.scripts.ps1.data import CallFact, CallKind, abbreviated_parameter, call_facts, named_type, resolve_type
from refinery.lib.scripts.ps1.dotnet import Ps1TypeName
from refinery.lib.scripts.ps1.model import (
Ps1AccessKind,
Ps1CommandArgument,
Ps1CommandArgumentKind,
Ps1CommandInvocation,
Ps1InvokeMember,
Ps1Pipeline,
Ps1PipelineElement,
Ps1TypeExpression,
)
_VOID = named_type('System.Void')
def _matching_fact(facts: tuple[CallFact, ...], arguments: list[Ps1Outcome] | None) -> CallFact | None:
"""
The first of *facts* that covers *arguments*, or `None` where none does. An argument a fact
covers is a value of one of the types the fact names at its position, which `$null` never is;
*arguments* is `None` for an argument list no fact can cover, and such a list is covered only by
a fact covering every argument list.
"""
for fact in facts:
covered = fact.arguments
if covered is None:
return fact
if arguments is None or len(arguments) != len(covered):
continue
if all(
not isinstance(types, str)
and (named := type_of(argument.value)) is not None
and named.generic_definition in types
for argument, types in zip(arguments, covered)
):
return fact
return None
class Ps1CallReader:
"""
The calls `refinery.lib.scripts.ps1.data.CALL_FACTS` covers, answered for one script under one
world. A static call is matched by the type literal it names, a call on a value by the type the
value is known to carry, and a command by its literal name. A command is matched only where it
writes to no redirection, is not reached through a call operator, and receives no pipeline
input, because every fact about a command was measured on a call standing on its own.
The world is held weakly, so that the reader `call_reader` keeps for a world never keeps that
world, or the tree it was measured over, alive. A reader whose world is gone trusts no position
and reads no fact. That is what the world itself answers once the tree changes under it, which
is how a pass holding the reader across its own edits outlives it, and it is never unsound.
"""
def __init__(self, world: Ps1WorldReach):
self._world = ref(world)
def _closed_at(self, call: Ps1InvokeMember) -> bool:
world = self._world()
return world is not None and world.closed_at(call)
def _may_trust_command_name_at(self, name: str, command: Ps1CommandInvocation) -> bool:
world = self._world()
return world is not None and world.may_trust_command_name_at(name, command)
def outcome(self, call: Ps1InvokeMember | Ps1CommandInvocation) -> Ps1Outcome:
"""
What *call* answers: the outcome its fact records once its receiver and arguments are
evaluated, or `NOTHING` where no fact covers it.
"""
if isinstance(call, Ps1InvokeMember):
return self._method(call)
return self._command(call)
def vouch(self, call: Ps1InvokeMember, receiver: Ps1TypeName) -> Ps1TypeName | None:
"""
The type a call on a value of *receiver* hands back where a fact covers it, never `$null`,
or `None` where no fact covers it. The receiver is typed by the caller, who can type a
variable where this cannot.
"""
if not self._may_trust_the_method(call) or call.access is not Ps1AccessKind.INSTANCE:
return None
assert isinstance(call.member, str)
fact = _matching_fact(
call_facts(CallKind.INSTANCE, receiver.generic_definition, call.member),
self._arguments(call),
)
if fact is None or fact.returns == _VOID:
return None
return fact.returns
def _may_trust_the_method(self, call: Ps1InvokeMember) -> bool:
return isinstance(call.member, str) and self._closed_at(call)
def _arguments(self, call: Ps1InvokeMember) -> list[Ps1Outcome]:
return [evaluate(argument, None, self) for argument in call.arguments]
def _method(self, call: Ps1InvokeMember) -> Ps1Outcome:
if not self._may_trust_the_method(call):
return NOTHING
assert isinstance(call.member, str)
if call.access is Ps1AccessKind.STATIC:
if not isinstance(call.object, Ps1TypeExpression):
return NOTHING
owner = resolve_type(call.object.name)
receiver = Ps1Outcome(Ps1Throws.NEVER, UNKNOWN)
kind = CallKind.STATIC
else:
receiver = evaluate(call.object, None, self)
owner = type_of(receiver.value)
kind = CallKind.INSTANCE
if owner is None:
return NOTHING
arguments = self._arguments(call)
fact = _matching_fact(call_facts(kind, owner.generic_definition, call.member), arguments)
return NOTHING if fact is None else self.recorded(fact, receiver, arguments)
def _command(self, command: Ps1CommandInvocation) -> Ps1Outcome:
if command.redirections or command.invocation_operator:
return NOTHING
name = get_command_name(command)
if name is None or not self._may_trust_command_name_at(name.lower(), command):
return NOTHING
piped = False
element = command.parent
if isinstance(element, Ps1PipelineElement):
pipeline = element.parent
if element.redirections or not isinstance(pipeline, Ps1Pipeline):
return NOTHING
piped = pipeline.elements[0] is not element
if session_of(command).defaults:
return NOTHING
tokens = self._tokens(command, name)
if tokens is None:
return NOTHING
informs = session_of(command).informs
for fact in call_facts(CallKind.COMMAND, None, name):
if piped and not fact.input:
continue
if informs and fact.name == 'write-host':
continue
values = _matching_tokens(fact, tokens)
if values is not None:
return self.recorded(fact, Ps1Outcome(Ps1Throws.NEVER, UNKNOWN), values)
return NOTHING
def _tokens(self, command: Ps1CommandInvocation, name: str) -> list[str | Ps1Outcome] | None:
tokens: list[str | Ps1Outcome] = []
for argument in command.arguments:
if not isinstance(argument, Ps1CommandArgument):
tokens.append(evaluate(argument, None, self))
continue
if argument.kind is Ps1CommandArgumentKind.POSITIONAL:
tokens.append(evaluate(argument.value, None, self))
continue
parameter = abbreviated_parameter(name, argument.name.rstrip(':'))
if parameter is None:
return None
tokens.append(parameter.lower())
if argument.kind is Ps1CommandArgumentKind.NAMED:
if argument.value is None:
return None
tokens.append(evaluate(argument.value, None, self))
return tokens
@staticmethod
def recorded(fact: CallFact, receiver: Ps1Outcome, arguments: list[Ps1Outcome]) -> Ps1Outcome:
"""
The outcome *fact* records for a call whose receiver and arguments evaluated to these: the
call fails where they may, and where the fact says it may.
"""
throws = throw_join(
receiver.throws,
*(argument.throws for argument in arguments),
Ps1Throws.MAYBE if fact.fails else Ps1Throws.NEVER,
)
if fact.returns is None:
return Ps1Outcome(throws, UNKNOWN)
return Ps1Outcome(throws, NULL if fact.returns == _VOID else Ps1Typed(fact.returns))
def _matching_tokens(fact: CallFact, tokens: list[str | Ps1Outcome]) -> list[Ps1Outcome] | None:
covered = fact.arguments
if covered is None or len(covered) != len(tokens):
return None
values: list[Ps1Outcome] = []
bounded = False
for item, token in zip(covered, tokens):
if isinstance(item, str):
if token != item:
return None
bounded = item == 'first'
continue
if isinstance(token, str):
return None
named = type_of(token.value)
if named is None or named.generic_definition not in item:
return None
if bounded:
value = token.value
if not isinstance(value, Ps1Constant) or not isinstance(value.payload, int) or value.payload < 0:
return None
values.append(token)
return values
def call_reader(world: Ps1WorldReach) -> Ps1CallReader:
"""
The one `Ps1CallReader` of *world*, so that what it answered is shared by every question asked
under that world.
"""
reader = _READERS.get(world)
if reader is None:
reader = _READERS[world] = Ps1CallReader(world)
return reader
_READERS: WeakKeyDictionary[Ps1WorldReach, Ps1CallReader] = WeakKeyDictionary()
Functions
def call_reader(world)-
The one
Ps1CallReaderof world, so that what it answered is shared by every question asked under that world.Expand source code Browse git
def call_reader(world: Ps1WorldReach) -> Ps1CallReader: """ The one `Ps1CallReader` of *world*, so that what it answered is shared by every question asked under that world. """ reader = _READERS.get(world) if reader is None: reader = _READERS[world] = Ps1CallReader(world) return reader
Classes
class Ps1CallReader (world)-
The calls
CALL_FACTScovers, answered for one script under one world. A static call is matched by the type literal it names, a call on a value by the type the value is known to carry, and a command by its literal name. A command is matched only where it writes to no redirection, is not reached through a call operator, and receives no pipeline input, because every fact about a command was measured on a call standing on its own.The world is held weakly, so that the reader
call_reader()keeps for a world never keeps that world, or the tree it was measured over, alive. A reader whose world is gone trusts no position and reads no fact. That is what the world itself answers once the tree changes under it, which is how a pass holding the reader across its own edits outlives it, and it is never unsound.Expand source code Browse git
class Ps1CallReader: """ The calls `refinery.lib.scripts.ps1.data.CALL_FACTS` covers, answered for one script under one world. A static call is matched by the type literal it names, a call on a value by the type the value is known to carry, and a command by its literal name. A command is matched only where it writes to no redirection, is not reached through a call operator, and receives no pipeline input, because every fact about a command was measured on a call standing on its own. The world is held weakly, so that the reader `call_reader` keeps for a world never keeps that world, or the tree it was measured over, alive. A reader whose world is gone trusts no position and reads no fact. That is what the world itself answers once the tree changes under it, which is how a pass holding the reader across its own edits outlives it, and it is never unsound. """ def __init__(self, world: Ps1WorldReach): self._world = ref(world) def _closed_at(self, call: Ps1InvokeMember) -> bool: world = self._world() return world is not None and world.closed_at(call) def _may_trust_command_name_at(self, name: str, command: Ps1CommandInvocation) -> bool: world = self._world() return world is not None and world.may_trust_command_name_at(name, command) def outcome(self, call: Ps1InvokeMember | Ps1CommandInvocation) -> Ps1Outcome: """ What *call* answers: the outcome its fact records once its receiver and arguments are evaluated, or `NOTHING` where no fact covers it. """ if isinstance(call, Ps1InvokeMember): return self._method(call) return self._command(call) def vouch(self, call: Ps1InvokeMember, receiver: Ps1TypeName) -> Ps1TypeName | None: """ The type a call on a value of *receiver* hands back where a fact covers it, never `$null`, or `None` where no fact covers it. The receiver is typed by the caller, who can type a variable where this cannot. """ if not self._may_trust_the_method(call) or call.access is not Ps1AccessKind.INSTANCE: return None assert isinstance(call.member, str) fact = _matching_fact( call_facts(CallKind.INSTANCE, receiver.generic_definition, call.member), self._arguments(call), ) if fact is None or fact.returns == _VOID: return None return fact.returns def _may_trust_the_method(self, call: Ps1InvokeMember) -> bool: return isinstance(call.member, str) and self._closed_at(call) def _arguments(self, call: Ps1InvokeMember) -> list[Ps1Outcome]: return [evaluate(argument, None, self) for argument in call.arguments] def _method(self, call: Ps1InvokeMember) -> Ps1Outcome: if not self._may_trust_the_method(call): return NOTHING assert isinstance(call.member, str) if call.access is Ps1AccessKind.STATIC: if not isinstance(call.object, Ps1TypeExpression): return NOTHING owner = resolve_type(call.object.name) receiver = Ps1Outcome(Ps1Throws.NEVER, UNKNOWN) kind = CallKind.STATIC else: receiver = evaluate(call.object, None, self) owner = type_of(receiver.value) kind = CallKind.INSTANCE if owner is None: return NOTHING arguments = self._arguments(call) fact = _matching_fact(call_facts(kind, owner.generic_definition, call.member), arguments) return NOTHING if fact is None else self.recorded(fact, receiver, arguments) def _command(self, command: Ps1CommandInvocation) -> Ps1Outcome: if command.redirections or command.invocation_operator: return NOTHING name = get_command_name(command) if name is None or not self._may_trust_command_name_at(name.lower(), command): return NOTHING piped = False element = command.parent if isinstance(element, Ps1PipelineElement): pipeline = element.parent if element.redirections or not isinstance(pipeline, Ps1Pipeline): return NOTHING piped = pipeline.elements[0] is not element if session_of(command).defaults: return NOTHING tokens = self._tokens(command, name) if tokens is None: return NOTHING informs = session_of(command).informs for fact in call_facts(CallKind.COMMAND, None, name): if piped and not fact.input: continue if informs and fact.name == 'write-host': continue values = _matching_tokens(fact, tokens) if values is not None: return self.recorded(fact, Ps1Outcome(Ps1Throws.NEVER, UNKNOWN), values) return NOTHING def _tokens(self, command: Ps1CommandInvocation, name: str) -> list[str | Ps1Outcome] | None: tokens: list[str | Ps1Outcome] = [] for argument in command.arguments: if not isinstance(argument, Ps1CommandArgument): tokens.append(evaluate(argument, None, self)) continue if argument.kind is Ps1CommandArgumentKind.POSITIONAL: tokens.append(evaluate(argument.value, None, self)) continue parameter = abbreviated_parameter(name, argument.name.rstrip(':')) if parameter is None: return None tokens.append(parameter.lower()) if argument.kind is Ps1CommandArgumentKind.NAMED: if argument.value is None: return None tokens.append(evaluate(argument.value, None, self)) return tokens @staticmethod def recorded(fact: CallFact, receiver: Ps1Outcome, arguments: list[Ps1Outcome]) -> Ps1Outcome: """ The outcome *fact* records for a call whose receiver and arguments evaluated to these: the call fails where they may, and where the fact says it may. """ throws = throw_join( receiver.throws, *(argument.throws for argument in arguments), Ps1Throws.MAYBE if fact.fails else Ps1Throws.NEVER, ) if fact.returns is None: return Ps1Outcome(throws, UNKNOWN) return Ps1Outcome(throws, NULL if fact.returns == _VOID else Ps1Typed(fact.returns))Static methods
def recorded(fact, receiver, arguments)-
The outcome fact records for a call whose receiver and arguments evaluated to these: the call fails where they may, and where the fact says it may.
Expand source code Browse git
@staticmethod def recorded(fact: CallFact, receiver: Ps1Outcome, arguments: list[Ps1Outcome]) -> Ps1Outcome: """ The outcome *fact* records for a call whose receiver and arguments evaluated to these: the call fails where they may, and where the fact says it may. """ throws = throw_join( receiver.throws, *(argument.throws for argument in arguments), Ps1Throws.MAYBE if fact.fails else Ps1Throws.NEVER, ) if fact.returns is None: return Ps1Outcome(throws, UNKNOWN) return Ps1Outcome(throws, NULL if fact.returns == _VOID else Ps1Typed(fact.returns))
Methods
def outcome(self, call)-
What call answers: the outcome its fact records once its receiver and arguments are evaluated, or
NOTHINGwhere no fact covers it.Expand source code Browse git
def outcome(self, call: Ps1InvokeMember | Ps1CommandInvocation) -> Ps1Outcome: """ What *call* answers: the outcome its fact records once its receiver and arguments are evaluated, or `NOTHING` where no fact covers it. """ if isinstance(call, Ps1InvokeMember): return self._method(call) return self._command(call) def vouch(self, call, receiver)-
The type a call on a value of receiver hands back where a fact covers it, never
$null, orNonewhere no fact covers it. The receiver is typed by the caller, who can type a variable where this cannot.Expand source code Browse git
def vouch(self, call: Ps1InvokeMember, receiver: Ps1TypeName) -> Ps1TypeName | None: """ The type a call on a value of *receiver* hands back where a fact covers it, never `$null`, or `None` where no fact covers it. The receiver is typed by the caller, who can type a variable where this cannot. """ if not self._may_trust_the_method(call) or call.access is not Ps1AccessKind.INSTANCE: return None assert isinstance(call.member, str) fact = _matching_fact( call_facts(CallKind.INSTANCE, receiver.generic_definition, call.member), self._arguments(call), ) if fact is None or fact.returns == _VOID: return None return fact.returns