Module refinery.lib.scripts.ps1.analysis.types
The expression-type oracle of the PowerShell analysis substrate: the .NET type a PowerShell
expression evaluates to, traced through member-access chains, or None when it cannot be
determined. It reads the collected type surface in refinery.lib.scripts.ps1.data and the syntactic
accessors in refinery.lib.scripts.ps1.ast, and nothing else, so it sits at the analysis level
where both the effect layer (refinery.lib.scripts.ps1.analysis.effects) and the deobfuscation
transforms can reach it without either importing the other.
resolve_expression_type() is the free-function core: one expression, one type or None. TypeOracle
is the value object the effect layer threads to reason about member reads. It carries the optional
variable and pipeline typing a semantic model supplies and exposes two views over one engine —
TypeOracle.candidate_types(), the set of types a value could have, and TypeOracle.resolve(), that set
collapsed to a single type when it is unambiguous. A method return or a declared cmdlet output can be
one of several types, which is why the set is the primitive and the single type the derived view.
Expand source code Browse git
"""
The expression-type oracle of the PowerShell analysis substrate: the .NET type a PowerShell
expression evaluates to, traced through member-access chains, or `None` when it cannot be
determined. It reads the collected type surface in `refinery.lib.scripts.ps1.data` and the syntactic
accessors in `refinery.lib.scripts.ps1.ast`, and nothing else, so it sits at the analysis level
where both the effect layer (`refinery.lib.scripts.ps1.analysis.effects`) and the deobfuscation
transforms can reach it without either importing the other.
`resolve_expression_type` is the free-function core: one expression, one type or `None`. `TypeOracle`
is the value object the effect layer threads to reason about member reads. It carries the optional
variable and pipeline typing a semantic model supplies and exposes two views over one engine —
`TypeOracle.candidate_types`, the set of types a value could have, and `TypeOracle.resolve`, that set
collapsed to a single type when it is unambiguous. A method return or a declared cmdlet output can be
one of several types, which is why the set is the primitive and the single type the derived view.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from refinery.lib.scripts.ps1.ast import (
extract_first_positional_string,
get_command_name,
get_member_name,
unwrap_parens,
)
from refinery.lib.scripts.ps1.data import (
OBJ_COMMANDS,
PROPERTY_TYPES,
TYPE_ARG_COMMANDS,
VARIABLE_TYPES,
WMI_CLASS_NAMES,
WMI_COMMANDS,
_resolve_type_name,
command_output_types,
static_overloads,
)
from refinery.lib.scripts.ps1.model import (
Expression,
Ps1AccessKind,
Ps1ArrayExpression,
Ps1ArrayLiteral,
Ps1CastExpression,
Ps1CommandInvocation,
Ps1ExpressionStatement,
Ps1HereString,
Ps1IntegerLiteral,
Ps1InvokeMember,
Ps1MemberAccess,
Ps1StringLiteral,
Ps1TypeExpression,
Ps1Variable,
)
if TYPE_CHECKING:
from refinery.lib.scripts.ps1.analysis.world import Ps1TypeWorld
def resolve_expression_type(
expr: Expression,
variable_types: dict[str, str] | None = None,
) -> str | None:
"""
Trace the .NET type of a PowerShell expression by walking member access chains. Returns the
lowercase full .NET type name, or `None` if the type cannot be determined.
"""
unwrapped = unwrap_parens(expr)
if not isinstance(unwrapped, Expression):
return None
expr = unwrapped
if isinstance(expr, (Ps1StringLiteral, Ps1HereString)):
return 'system.string'
if isinstance(expr, Ps1IntegerLiteral):
return 'system.int32'
if isinstance(expr, Ps1ArrayLiteral):
return 'system.array'
if isinstance(expr, Ps1ArrayExpression):
if (
len(expr.body) == 1
and isinstance(expr.body[0], Ps1ExpressionStatement)
and isinstance(expr.body[0].expression, Ps1ArrayLiteral)
):
return 'system.array'
if isinstance(expr, Ps1Variable):
key = expr.name.lower()
if variable_types and key in variable_types:
return variable_types[key]
return VARIABLE_TYPES.get(key)
if isinstance(expr, Ps1TypeExpression):
return _resolve_type_name(expr.name)
if isinstance(expr, Ps1CastExpression):
return _resolve_type_name(expr.type_name)
if isinstance(expr, Ps1CommandInvocation):
cmd_name = get_command_name(expr)
if cmd_name is not None:
cmd_lower = cmd_name.lower()
if cmd_lower in OBJ_COMMANDS:
type_str = extract_first_positional_string(expr)
if type_str is not None:
return _resolve_type_name(type_str)
elif cmd_lower in WMI_COMMANDS:
class_str = extract_first_positional_string(expr)
if class_str is not None:
wmi_lower = class_str.lower()
if wmi_lower in WMI_CLASS_NAMES:
return wmi_lower
if isinstance(expr, Ps1MemberAccess):
if expr.object is None:
return None
obj_type = resolve_expression_type(expr.object, variable_types)
if obj_type is None:
return None
member_name = get_member_name(expr.member)
if member_name is None:
return None
return PROPERTY_TYPES.get((obj_type, member_name.lower()))
return None
#: Commands whose declared `[OutputType]` is a trustworthy *superset* of what they emit at runtime,
#: not merely a lower bound. Most commands under-declare: one that forwards its input emits the
#: input's type, which it never lists — `Get-Random -InputObject $procs` returns a `Process`,
#: `Get-Content` on a non-filesystem provider returns whatever that provider yields — so trusting
#: the declaration lets the member gate prove `(...).Path` pure over an incomplete candidate set and
#: delete a live effect. Only commands that emit their own output and cannot pass input through
#: belong here; a read on any other command's result stays unresolved, and therefore kept.
_CLOSED_OUTPUT_CMDLETS = frozenset({
'get-date',
})
class TypeOracle:
"""
The type view the effect layer consults to decide whether reading a member has a side effect. It
resolves a PowerShell expression to the .NET types its value could have, over one engine in two
views: `candidate_types` yields every possibility, and `resolve` collapses them to a single type
when that is unambiguous.
The oracle carries whatever variable and pipeline typing a semantic model has derived; with none
it still resolves everything the static surface alone determines: literals, casts, `New-Object`,
WMI queries, the return of a static method call and a cmdlet's declared outputs. A populated
oracle only adds the reads whose object is a typed variable or a pipeline item.
It also carries the two command-table facts the gate consults: `world_closed_at`, whether the
script's type system is unmutated (a present-member purity grant requires it), and
`may_trust_command_name`, whether the metadata still describes what a command name runs (a grant
must decline it when it does not). Both read the same world and both withhold when there is
none, so a caller can never obtain a grant from one that the other would refuse.
"""
def __init__(
self,
variable_types: dict[str, str] | None = None,
world: Ps1TypeWorld | None = None,
):
self._variable_types = variable_types
self._world = world
def with_variable_types(self, variable_types: dict[str, str] | None) -> TypeOracle:
"""
This oracle with `variable_types` substituted, carrying the same world. The world is a
whole-script fact and the typing is not, so a caller holding typing derived for one node
layers it here rather than building a second oracle — which would be free to answer the
world questions differently, and is how one script comes to have two disagreeing verdicts.
"""
return TypeOracle(variable_types, self._world)
def world_closed_at(self, node) -> bool:
"""
Whether the .NET type world is closed at `node`: no code the script runs can have shadowed a
member through the Extended Type System or remapped a type accelerator, so a present-member
purity grant can be trusted. This delegates to the
`refinery.lib.scripts.ps1.analysis.world.Ps1TypeWorld` a model has supplied; an oracle
built without one answers `False`, so the member gate keeps every access.
"""
return self._world is not None and self._world.world_closed_at(node)
def may_trust_command_name(self, name: str, node) -> bool:
"""
Whether the collected metadata still describes what the command `name` runs at `node`, so a
site may act on the name — for typing or for purity. Two things stop it describing it, and
both are the world's to answer: the script redefines the name where the walk can classify
the redefinition, or the world is open at `node`, in which case a dot-sourced file, an
imported module, an `iex`, an item cmdlet writing the `function:` provider or an opaque
dispatch can bind *any* name to code this tree does not contain. Reading the shadow set
alone would trust every name in exactly the scripts able to rebind them, and that set holds
only the two spellings the classifier sees. An oracle built without a world trusts no name,
matching `world_closed_at`: both questions withhold when there is nothing to ask.
Named for the question a caller actually has rather than for the shadow set, because the
answer is wider than the set: a reader who takes this for "is it redefined?" and narrows it
back to that would reopen a hole that deletes code, and no `_grant` sits in the path to
catch it.
"""
if self._world is None:
return False
return self._world.world_closed_at(node) and not self._world.command_shadowed(name)
def resolve(self, expr: Expression) -> str | None:
"""
The single lowercase .NET type name the expression evaluates to when that is unambiguous,
else `None`: `candidate_types` collapsed, so a lone candidate is the answer and zero or
several are `None`. This is a strict superset of `resolve_expression_type` — it resolves every
expression that one does and, additionally, a static method call or cmdlet whose result is a
single type — so putting it into a transform in place of the free function would widen what
resolves, which is deliberately not done where existing output depends on the narrower answer.
"""
candidates = self.candidate_types(expr)
if len(candidates) == 1:
return next(iter(candidates))
return None
def candidate_types(self, expr: Expression) -> frozenset[str]:
"""
The set of lowercase full .NET type names the expression's value could have, each a key the
type metadata resolves, or the empty set when the type cannot be determined. A static method
call contributes the return its overloads agree on, and a cmdlet call the output types it
declares; either can be several, so a caller reasoning about the value must have its
conclusion hold for every candidate. The single-type forms — literals, variables, casts,
`New-Object`, WMI, and property chains — are delegated to `resolve_expression_type` rather
than re-derived here.
"""
unwrapped = unwrap_parens(expr)
if not isinstance(unwrapped, Expression):
return frozenset()
expr = unwrapped
if isinstance(expr, Ps1InvokeMember):
return self._static_method_candidates(expr)
if isinstance(expr, Ps1CommandInvocation):
return self._command_candidates(expr)
single = resolve_expression_type(expr, self._variable_types)
return frozenset() if single is None else frozenset({single})
def _static_method_candidates(self, node: Ps1InvokeMember) -> frozenset[str]:
"""
The return type of a `[Type]::Method(...)` call, taken only when every matching static
overload agrees on it; disagreement or an unrecognized call is the empty set. An instance
method call is not resolved — its receiver type would have to be traced and its overloads
selected by argument type — so it contributes nothing rather than a guess.
"""
if node.access is not Ps1AccessKind.STATIC:
return frozenset()
obj = node.object
member = node.member
if not isinstance(obj, Ps1TypeExpression) or not isinstance(member, str):
return frozenset()
returns = {
overload['returns'].lower()
for overload in static_overloads(obj.name, member)
if overload.get('returns')
}
return frozenset(returns) if len(returns) == 1 else frozenset()
def _command_candidates(self, cmd: Ps1CommandInvocation) -> frozenset[str]:
"""
The types a command's result could have: the constructed or queried type for the `New-Object`
and WMI forms the single-type ladder already knows, otherwise the output types a command
declares through `[OutputType]` — but only for a command whose declaration is a trustworthy
*superset* of what it emits (`_CLOSED_OUTPUT_CMDLETS`). `[OutputType]` is a lower bound in
general: a command that forwards its input emits types it never declares, and trusting the
declaration there would let the member gate prove an effectful read pure over an incomplete
candidate set. Every other command contributes nothing, so a read on its result stays
unresolved and is kept.
"""
name = get_command_name(cmd)
if name is None:
return frozenset()
lower = name.lower()
if not self.may_trust_command_name(lower, cmd):
return frozenset()
if lower in TYPE_ARG_COMMANDS:
single = resolve_expression_type(cmd, self._variable_types)
return frozenset() if single is None else frozenset({single})
if lower not in _CLOSED_OUTPUT_CMDLETS:
return frozenset()
declared = command_output_types(name)
return declared if declared is not None else frozenset()
Functions
def resolve_expression_type(expr, variable_types=None)-
Trace the .NET type of a PowerShell expression by walking member access chains. Returns the lowercase full .NET type name, or
Noneif the type cannot be determined.Expand source code Browse git
def resolve_expression_type( expr: Expression, variable_types: dict[str, str] | None = None, ) -> str | None: """ Trace the .NET type of a PowerShell expression by walking member access chains. Returns the lowercase full .NET type name, or `None` if the type cannot be determined. """ unwrapped = unwrap_parens(expr) if not isinstance(unwrapped, Expression): return None expr = unwrapped if isinstance(expr, (Ps1StringLiteral, Ps1HereString)): return 'system.string' if isinstance(expr, Ps1IntegerLiteral): return 'system.int32' if isinstance(expr, Ps1ArrayLiteral): return 'system.array' if isinstance(expr, Ps1ArrayExpression): if ( len(expr.body) == 1 and isinstance(expr.body[0], Ps1ExpressionStatement) and isinstance(expr.body[0].expression, Ps1ArrayLiteral) ): return 'system.array' if isinstance(expr, Ps1Variable): key = expr.name.lower() if variable_types and key in variable_types: return variable_types[key] return VARIABLE_TYPES.get(key) if isinstance(expr, Ps1TypeExpression): return _resolve_type_name(expr.name) if isinstance(expr, Ps1CastExpression): return _resolve_type_name(expr.type_name) if isinstance(expr, Ps1CommandInvocation): cmd_name = get_command_name(expr) if cmd_name is not None: cmd_lower = cmd_name.lower() if cmd_lower in OBJ_COMMANDS: type_str = extract_first_positional_string(expr) if type_str is not None: return _resolve_type_name(type_str) elif cmd_lower in WMI_COMMANDS: class_str = extract_first_positional_string(expr) if class_str is not None: wmi_lower = class_str.lower() if wmi_lower in WMI_CLASS_NAMES: return wmi_lower if isinstance(expr, Ps1MemberAccess): if expr.object is None: return None obj_type = resolve_expression_type(expr.object, variable_types) if obj_type is None: return None member_name = get_member_name(expr.member) if member_name is None: return None return PROPERTY_TYPES.get((obj_type, member_name.lower())) return None
Classes
class TypeOracle (variable_types=None, world=None)-
The type view the effect layer consults to decide whether reading a member has a side effect. It resolves a PowerShell expression to the .NET types its value could have, over one engine in two views:
candidate_typesyields every possibility, andresolvecollapses them to a single type when that is unambiguous.The oracle carries whatever variable and pipeline typing a semantic model has derived; with none it still resolves everything the static surface alone determines: literals, casts,
New-Object, WMI queries, the return of a static method call and a cmdlet's declared outputs. A populated oracle only adds the reads whose object is a typed variable or a pipeline item.It also carries the two command-table facts the gate consults:
world_closed_at, whether the script's type system is unmutated (a present-member purity grant requires it), andmay_trust_command_name, whether the metadata still describes what a command name runs (a grant must decline it when it does not). Both read the same world and both withhold when there is none, so a caller can never obtain a grant from one that the other would refuse.Expand source code Browse git
class TypeOracle: """ The type view the effect layer consults to decide whether reading a member has a side effect. It resolves a PowerShell expression to the .NET types its value could have, over one engine in two views: `candidate_types` yields every possibility, and `resolve` collapses them to a single type when that is unambiguous. The oracle carries whatever variable and pipeline typing a semantic model has derived; with none it still resolves everything the static surface alone determines: literals, casts, `New-Object`, WMI queries, the return of a static method call and a cmdlet's declared outputs. A populated oracle only adds the reads whose object is a typed variable or a pipeline item. It also carries the two command-table facts the gate consults: `world_closed_at`, whether the script's type system is unmutated (a present-member purity grant requires it), and `may_trust_command_name`, whether the metadata still describes what a command name runs (a grant must decline it when it does not). Both read the same world and both withhold when there is none, so a caller can never obtain a grant from one that the other would refuse. """ def __init__( self, variable_types: dict[str, str] | None = None, world: Ps1TypeWorld | None = None, ): self._variable_types = variable_types self._world = world def with_variable_types(self, variable_types: dict[str, str] | None) -> TypeOracle: """ This oracle with `variable_types` substituted, carrying the same world. The world is a whole-script fact and the typing is not, so a caller holding typing derived for one node layers it here rather than building a second oracle — which would be free to answer the world questions differently, and is how one script comes to have two disagreeing verdicts. """ return TypeOracle(variable_types, self._world) def world_closed_at(self, node) -> bool: """ Whether the .NET type world is closed at `node`: no code the script runs can have shadowed a member through the Extended Type System or remapped a type accelerator, so a present-member purity grant can be trusted. This delegates to the `refinery.lib.scripts.ps1.analysis.world.Ps1TypeWorld` a model has supplied; an oracle built without one answers `False`, so the member gate keeps every access. """ return self._world is not None and self._world.world_closed_at(node) def may_trust_command_name(self, name: str, node) -> bool: """ Whether the collected metadata still describes what the command `name` runs at `node`, so a site may act on the name — for typing or for purity. Two things stop it describing it, and both are the world's to answer: the script redefines the name where the walk can classify the redefinition, or the world is open at `node`, in which case a dot-sourced file, an imported module, an `iex`, an item cmdlet writing the `function:` provider or an opaque dispatch can bind *any* name to code this tree does not contain. Reading the shadow set alone would trust every name in exactly the scripts able to rebind them, and that set holds only the two spellings the classifier sees. An oracle built without a world trusts no name, matching `world_closed_at`: both questions withhold when there is nothing to ask. Named for the question a caller actually has rather than for the shadow set, because the answer is wider than the set: a reader who takes this for "is it redefined?" and narrows it back to that would reopen a hole that deletes code, and no `_grant` sits in the path to catch it. """ if self._world is None: return False return self._world.world_closed_at(node) and not self._world.command_shadowed(name) def resolve(self, expr: Expression) -> str | None: """ The single lowercase .NET type name the expression evaluates to when that is unambiguous, else `None`: `candidate_types` collapsed, so a lone candidate is the answer and zero or several are `None`. This is a strict superset of `resolve_expression_type` — it resolves every expression that one does and, additionally, a static method call or cmdlet whose result is a single type — so putting it into a transform in place of the free function would widen what resolves, which is deliberately not done where existing output depends on the narrower answer. """ candidates = self.candidate_types(expr) if len(candidates) == 1: return next(iter(candidates)) return None def candidate_types(self, expr: Expression) -> frozenset[str]: """ The set of lowercase full .NET type names the expression's value could have, each a key the type metadata resolves, or the empty set when the type cannot be determined. A static method call contributes the return its overloads agree on, and a cmdlet call the output types it declares; either can be several, so a caller reasoning about the value must have its conclusion hold for every candidate. The single-type forms — literals, variables, casts, `New-Object`, WMI, and property chains — are delegated to `resolve_expression_type` rather than re-derived here. """ unwrapped = unwrap_parens(expr) if not isinstance(unwrapped, Expression): return frozenset() expr = unwrapped if isinstance(expr, Ps1InvokeMember): return self._static_method_candidates(expr) if isinstance(expr, Ps1CommandInvocation): return self._command_candidates(expr) single = resolve_expression_type(expr, self._variable_types) return frozenset() if single is None else frozenset({single}) def _static_method_candidates(self, node: Ps1InvokeMember) -> frozenset[str]: """ The return type of a `[Type]::Method(...)` call, taken only when every matching static overload agrees on it; disagreement or an unrecognized call is the empty set. An instance method call is not resolved — its receiver type would have to be traced and its overloads selected by argument type — so it contributes nothing rather than a guess. """ if node.access is not Ps1AccessKind.STATIC: return frozenset() obj = node.object member = node.member if not isinstance(obj, Ps1TypeExpression) or not isinstance(member, str): return frozenset() returns = { overload['returns'].lower() for overload in static_overloads(obj.name, member) if overload.get('returns') } return frozenset(returns) if len(returns) == 1 else frozenset() def _command_candidates(self, cmd: Ps1CommandInvocation) -> frozenset[str]: """ The types a command's result could have: the constructed or queried type for the `New-Object` and WMI forms the single-type ladder already knows, otherwise the output types a command declares through `[OutputType]` — but only for a command whose declaration is a trustworthy *superset* of what it emits (`_CLOSED_OUTPUT_CMDLETS`). `[OutputType]` is a lower bound in general: a command that forwards its input emits types it never declares, and trusting the declaration there would let the member gate prove an effectful read pure over an incomplete candidate set. Every other command contributes nothing, so a read on its result stays unresolved and is kept. """ name = get_command_name(cmd) if name is None: return frozenset() lower = name.lower() if not self.may_trust_command_name(lower, cmd): return frozenset() if lower in TYPE_ARG_COMMANDS: single = resolve_expression_type(cmd, self._variable_types) return frozenset() if single is None else frozenset({single}) if lower not in _CLOSED_OUTPUT_CMDLETS: return frozenset() declared = command_output_types(name) return declared if declared is not None else frozenset()Methods
def with_variable_types(self, variable_types)-
This oracle with
variable_typessubstituted, carrying the same world. The world is a whole-script fact and the typing is not, so a caller holding typing derived for one node layers it here rather than building a second oracle — which would be free to answer the world questions differently, and is how one script comes to have two disagreeing verdicts.Expand source code Browse git
def with_variable_types(self, variable_types: dict[str, str] | None) -> TypeOracle: """ This oracle with `variable_types` substituted, carrying the same world. The world is a whole-script fact and the typing is not, so a caller holding typing derived for one node layers it here rather than building a second oracle — which would be free to answer the world questions differently, and is how one script comes to have two disagreeing verdicts. """ return TypeOracle(variable_types, self._world) def world_closed_at(self, node)-
Whether the .NET type world is closed at
node: no code the script runs can have shadowed a member through the Extended Type System or remapped a type accelerator, so a present-member purity grant can be trusted. This delegates to thePs1TypeWorlda model has supplied; an oracle built without one answersFalse, so the member gate keeps every access.Expand source code Browse git
def world_closed_at(self, node) -> bool: """ Whether the .NET type world is closed at `node`: no code the script runs can have shadowed a member through the Extended Type System or remapped a type accelerator, so a present-member purity grant can be trusted. This delegates to the `refinery.lib.scripts.ps1.analysis.world.Ps1TypeWorld` a model has supplied; an oracle built without one answers `False`, so the member gate keeps every access. """ return self._world is not None and self._world.world_closed_at(node) def may_trust_command_name(self, name, node)-
Whether the collected metadata still describes what the command
nameruns atnode, so a site may act on the name — for typing or for purity. Two things stop it describing it, and both are the world's to answer: the script redefines the name where the walk can classify the redefinition, or the world is open atnode, in which case a dot-sourced file, an imported module, aniex, an item cmdlet writing thefunction:provider or an opaque dispatch can bind any name to code this tree does not contain. Reading the shadow set alone would trust every name in exactly the scripts able to rebind them, and that set holds only the two spellings the classifier sees. An oracle built without a world trusts no name, matchingworld_closed_at: both questions withhold when there is nothing to ask.Named for the question a caller actually has rather than for the shadow set, because the answer is wider than the set: a reader who takes this for "is it redefined?" and narrows it back to that would reopen a hole that deletes code, and no
_grantsits in the path to catch it.Expand source code Browse git
def may_trust_command_name(self, name: str, node) -> bool: """ Whether the collected metadata still describes what the command `name` runs at `node`, so a site may act on the name — for typing or for purity. Two things stop it describing it, and both are the world's to answer: the script redefines the name where the walk can classify the redefinition, or the world is open at `node`, in which case a dot-sourced file, an imported module, an `iex`, an item cmdlet writing the `function:` provider or an opaque dispatch can bind *any* name to code this tree does not contain. Reading the shadow set alone would trust every name in exactly the scripts able to rebind them, and that set holds only the two spellings the classifier sees. An oracle built without a world trusts no name, matching `world_closed_at`: both questions withhold when there is nothing to ask. Named for the question a caller actually has rather than for the shadow set, because the answer is wider than the set: a reader who takes this for "is it redefined?" and narrows it back to that would reopen a hole that deletes code, and no `_grant` sits in the path to catch it. """ if self._world is None: return False return self._world.world_closed_at(node) and not self._world.command_shadowed(name) def resolve(self, expr)-
The single lowercase .NET type name the expression evaluates to when that is unambiguous, else
None:candidate_typescollapsed, so a lone candidate is the answer and zero or several areNone. This is a strict superset ofresolve_expression_type()— it resolves every expression that one does and, additionally, a static method call or cmdlet whose result is a single type — so putting it into a transform in place of the free function would widen what resolves, which is deliberately not done where existing output depends on the narrower answer.Expand source code Browse git
def resolve(self, expr: Expression) -> str | None: """ The single lowercase .NET type name the expression evaluates to when that is unambiguous, else `None`: `candidate_types` collapsed, so a lone candidate is the answer and zero or several are `None`. This is a strict superset of `resolve_expression_type` — it resolves every expression that one does and, additionally, a static method call or cmdlet whose result is a single type — so putting it into a transform in place of the free function would widen what resolves, which is deliberately not done where existing output depends on the narrower answer. """ candidates = self.candidate_types(expr) if len(candidates) == 1: return next(iter(candidates)) return None def candidate_types(self, expr)-
The set of lowercase full .NET type names the expression's value could have, each a key the type metadata resolves, or the empty set when the type cannot be determined. A static method call contributes the return its overloads agree on, and a cmdlet call the output types it declares; either can be several, so a caller reasoning about the value must have its conclusion hold for every candidate. The single-type forms — literals, variables, casts,
New-Object, WMI, and property chains — are delegated toresolve_expression_type()rather than re-derived here.Expand source code Browse git
def candidate_types(self, expr: Expression) -> frozenset[str]: """ The set of lowercase full .NET type names the expression's value could have, each a key the type metadata resolves, or the empty set when the type cannot be determined. A static method call contributes the return its overloads agree on, and a cmdlet call the output types it declares; either can be several, so a caller reasoning about the value must have its conclusion hold for every candidate. The single-type forms — literals, variables, casts, `New-Object`, WMI, and property chains — are delegated to `resolve_expression_type` rather than re-derived here. """ unwrapped = unwrap_parens(expr) if not isinstance(unwrapped, Expression): return frozenset() expr = unwrapped if isinstance(expr, Ps1InvokeMember): return self._static_method_candidates(expr) if isinstance(expr, Ps1CommandInvocation): return self._command_candidates(expr) single = resolve_expression_type(expr, self._variable_types) return frozenset() if single is None else frozenset({single})