from __future__ import annotations
import collections
import logging
import pathlib
import textwrap
from dataclasses import dataclass, field, fields, is_dataclass
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Union
import lark
from . import transform as tf
from .parse import ParseResult
from .typing import Literal
from .util import Identifier, SourceType
LocationType = Literal["input", "output", "memory"]
logger = logging.getLogger(__name__)
def _indented_outline(item: Any, indent: str = " ") -> Optional[str]:
"""Outline and indent the given item."""
text = text_outline(item)
if text is None:
return None
result = textwrap.indent(text, indent)
if "\n" in result:
return "\n" + result
return result.lstrip()
[docs]
def text_outline(item: Any) -> Optional[str]:
"""
Get a generic multiline string representation of the given object.
Attempts to include field information for dataclasses, put list items
on separate lines, and generally keep sensible indentation.
Parameters
----------
item : Any
The item to outline.
Returns
-------
formatted : str or None
The formatted result.
"""
if item is None:
return None
if is_dataclass(item):
result = [
f"<{item.__class__.__name__}>"
]
for fld in fields(item):
if fld.name in ("meta", ):
continue
value = _indented_outline(getattr(item, fld.name, None))
if value is not None:
result.append(f"{fld.name}: {value}")
if not result:
return None
return "\n".join(result)
if isinstance(item, (list, tuple)):
result = []
for value in item:
value = _indented_outline(value)
if value is not None:
result.append(f"- {value.lstrip()}")
if not result:
return None
return "\n".join(result)
if isinstance(item, dict):
result = []
for key, value in item.items():
value = _indented_outline(value)
if value is not None:
result.append(f"- {key}: {value}")
return "\n".join(result)
return str(item)
[docs]
@dataclass
class Summary:
"""Base class for summary objects."""
comments: List[str]
pragmas: List[str]
filename: Optional[pathlib.Path]
meta: Optional[tf.Meta] = field(repr=False)
def __getitem__(self, _: str) -> None:
return None
def __str__(self) -> str:
return text_outline(self) or ""
[docs]
@dataclass
class DeclarationSummary(Summary):
"""Summary representation of a single declaration."""
name: str
item: Union[
tf.Declaration,
tf.GlobalVariableDeclaration,
tf.VariableInitDeclaration,
tf.StructureElementDeclaration,
tf.UnionElementDeclaration,
tf.ExternalVariableDeclaration,
tf.InitDeclaration,
]
parent: Optional[str]
location: Optional[str]
block: str
base_type: str
type: str
value: Optional[str]
def __getitem__(self, key: str) -> None:
raise KeyError(f"{self.name}[{key!r}]: declarations do not contain keys")
@property
def qualified_name(self) -> str:
"""Qualified name including parent. For example, ``fbName.DeclName``."""
if self.parent:
return f"{self.parent}.{self.name}"
return self.name
@property
def location_type(self) -> Optional[LocationType]:
"""If located, one of {'input', 'output', 'memory"}."""
if not self.location:
return None
location = self.location.upper()
if location.startswith("%I"):
return "input"
if location.startswith("%Q"):
return "output"
if location.startswith("%M"):
return "memory"
return None
[docs]
@classmethod
def from_declaration(
cls,
item: Union[tf.InitDeclaration, tf.StructureElementDeclaration, tf.UnionElementDeclaration],
parent: Optional[
Union[tf.Function, tf.Method, tf.FunctionBlock, tf.StructureTypeDeclaration]
] = None,
block_header: str = "unknown",
filename: Optional[pathlib.Path] = None,
) -> Dict[str, DeclarationSummary]:
result = {}
if isinstance(item, tf.StructureElementDeclaration):
result[item.name] = DeclarationSummary(
name=str(item.name),
item=item,
location=item.location.name if item.location else None,
block=block_header,
type=item.full_type_name, # TODO -> get_type_summary?
base_type=item.base_type_name,
value=str(item.value),
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
elif isinstance(item, tf.UnionElementDeclaration):
result[item.name] = DeclarationSummary(
name=str(item.name),
item=item,
location=None,
block=block_header,
type=item.spec.full_type_name,
base_type=item.spec.base_type_name,
value="",
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
elif isinstance(
item,
(
tf.FunctionBlockNameDeclaration,
tf.FunctionBlockInvocationDeclaration,
),
):
for var in item.variables:
result[var] = DeclarationSummary(
name=str(var),
item=item,
location=None,
block=block_header,
type=item.init.full_type_name,
base_type=item.init.base_type_name,
value=str(item.init.value),
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
elif isinstance(item, tf.InitDeclaration):
for var in item.variables:
result[var.name] = DeclarationSummary(
name=str(var.name),
item=item,
location=str(var.location).replace("AT ", "") if var.location else None,
block=block_header,
type=item.init.full_type_name,
base_type=item.init.base_type_name,
value=str(item.init.value),
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
elif isinstance(item, tf.ExternalVariableDeclaration):
location = str(getattr(item.spec, "location", None))
init = str(getattr(item.spec, "init", None))
type_ = getattr(init, "full_type_name", str(item.spec))
base_type = getattr(init, "base_type_name", str(item.spec))
result[item.name] = DeclarationSummary(
name=str(item.name),
item=item,
location=location,
block=block_header,
type=type_,
base_type=base_type,
value="None", # TODO
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
else:
raise NotImplementedError(f"TODO: {type(item)}")
return result
[docs]
@classmethod
def from_global_variable(
cls,
item: tf.GlobalVariableDeclaration,
parent: Optional[tf.GlobalVariableDeclarations] = None,
block_header: str = "VAR_GLOBAL",
filename: Optional[pathlib.Path] = None,
) -> Dict[str, DeclarationSummary]:
result = {}
location = (str(item.spec.location or "").replace("AT ", "")) or None
for var in item.spec.variables:
name = getattr(var, "name", var)
result[name] = DeclarationSummary(
name=str(name),
item=item,
location=location,
block=block_header,
type=item.full_type_name,
base_type=item.base_type_name,
value=str(item.init.value),
parent=parent.name if parent is not None else "",
filename=filename,
**Summary.get_meta_kwargs(item.meta),
)
return result
[docs]
@classmethod
def from_block(
cls,
block: tf.VariableDeclarationBlock,
parent: Union[tf.Function, tf.Method, tf.FunctionBlock],
filename: Optional[pathlib.Path] = None,
) -> Dict[str, DeclarationSummary]:
result = {}
for decl in block.items:
result.update(
cls.from_declaration(
decl,
parent=parent,
block_header=block.block_header,
filename=filename,
)
)
return result
[docs]
@dataclass
class ActionSummary(Summary):
"""Summary representation of a single action."""
name: str
item: tf.Action
source_code: str
implementation: Optional[tf.StatementList] = None
def __getitem__(self, key: str) -> None:
raise KeyError(f"{key}: Actions do not contain declarations")
[docs]
@classmethod
def from_statement_list(
cls,
name: str,
statements: tf.StatementList,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> ActionSummary:
if source_code is None:
source_code = str(statements)
return ActionSummary(
name=name,
item=tf.Action(
name=lark.Token(name, name),
body=statements,
meta=statements.meta,
),
source_code=source_code,
filename=filename,
implementation=statements, # TODO: this is no good
**Summary.get_meta_kwargs(statements.meta),
)
[docs]
@classmethod
def from_action(
cls,
action: tf.Action,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> ActionSummary:
if source_code is None:
source_code = str(action)
return ActionSummary(
name=str(action.name),
item=action,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(action.meta),
)
[docs]
@dataclass
class MethodSummary(Summary):
"""Summary representation of a single method."""
name: str
item: tf.Method
return_type: Optional[str]
source_code: str
implementation: Optional[tf.StatementList] = None
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
def __getitem__(self, key: str) -> DeclarationSummary:
return self.declarations[key]
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
result = {}
for decl in self.declarations.values():
result.setdefault(decl.block, {})[decl.name] = decl
return result
[docs]
@classmethod
def from_method(
cls,
method: tf.Method,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> MethodSummary:
if source_code is None:
source_code = str(method)
summary = MethodSummary(
name=method.name,
item=method,
return_type=str(method.return_type) if method.return_type else None,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(method.meta),
)
for decl in method.declarations:
summary.declarations.update(
DeclarationSummary.from_block(decl, parent=method, filename=filename)
)
return summary
[docs]
@dataclass
class PropertyGetSetSummary(Summary):
name: str
item: tf.Property
source_code: str
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
implementation: Optional[tf.StatementList] = None
def __getitem__(self, key: str) -> DeclarationSummary:
return self.declarations[key]
[docs]
@dataclass
class PropertySummary(Summary):
"""Summary representation of a single property."""
name: str
getter: PropertyGetSetSummary
setter: PropertyGetSetSummary
source_code: str
# implementation: Optional[tf.StatementList] = None
def __getitem__(self, key: str):
if key == "get":
return self.getter
if key == "set":
return self.setter
raise KeyError(f"{key}: Properties do not contain declarations")
[docs]
@classmethod
def from_property(
cls,
property: tf.Property,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> PropertySummary:
if source_code is None:
source_code = str(property)
# TODO: this is broken at the moment
return PropertySummary(
name=str(property.name),
getter=PropertyGetSetSummary(
name=str(property.name),
item=property,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(property.meta),
),
setter=PropertyGetSetSummary(
name=str(property.name),
item=property,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(property.meta),
),
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(property.meta),
)
[docs]
@dataclass
class FunctionSummary(Summary):
"""Summary representation of a single function."""
name: str
item: tf.Function
return_type: Optional[str]
source_code: str
implementation: Optional[tf.StatementList] = None
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
def __getitem__(self, key: str) -> DeclarationSummary:
return self.declarations[key]
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
result = {}
for decl in self.declarations.values():
result.setdefault(decl.block, {})[decl.name] = decl
return result
[docs]
@classmethod
def from_function(
cls,
func: tf.Function,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> FunctionSummary:
if source_code is None:
source_code = str(func)
summary = FunctionSummary(
name=func.name,
item=func,
return_type=str(func.return_type) if func.return_type else None,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(func.meta),
)
for decl in func.declarations:
summary.declarations.update(
DeclarationSummary.from_block(decl, parent=func, filename=filename)
)
return summary
[docs]
@dataclass
class FunctionBlockSummary(Summary):
"""Summary representation of a single function block."""
name: str
source_code: str
item: tf.FunctionBlock
extends: Optional[str]
squashed: bool
implementation: Optional[tf.StatementList] = None
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
actions: List[ActionSummary] = field(default_factory=list)
methods: List[MethodSummary] = field(default_factory=list)
properties: List[PropertySummary] = field(default_factory=list)
def __getitem__(
self, key: str
) -> Union[DeclarationSummary, MethodSummary, PropertySummary, ActionSummary]:
if key in self.declarations:
return self.declarations[key]
for item in self.actions + self.methods + self.properties:
if item.name == key:
return item
raise KeyError(key)
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
result = {}
for decl in self.declarations.values():
result.setdefault(decl.block, {})[decl.name] = decl
return result
[docs]
@classmethod
def from_function_block(
cls,
fb: tf.FunctionBlock,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> FunctionBlockSummary:
if source_code is None:
source_code = str(fb)
summary = FunctionBlockSummary(
name=fb.name,
item=fb,
source_code=source_code,
filename=filename,
extends=fb.extends.name if fb.extends else None,
squashed=False,
**Summary.get_meta_kwargs(fb.meta),
)
for decl in fb.declarations:
summary.declarations.update(
DeclarationSummary.from_block(decl, parent=fb, filename=filename)
)
return summary
[docs]
def squash_base_extends(
self, function_blocks: Dict[str, FunctionBlockSummary]
) -> FunctionBlockSummary:
"""Squash the "EXTENDS" function block into this one."""
if self.extends is None:
return self
extends_from = function_blocks.get(str(self.extends), None)
if extends_from is None:
return self
if extends_from.extends:
extends_from = extends_from.squash_base_extends(function_blocks)
declarations = dict(extends_from.declarations)
declarations.update(self.declarations)
actions = list(extends_from.actions) + self.actions
methods = list(extends_from.methods) + self.methods
properties = list(extends_from.properties) + self.properties
return FunctionBlockSummary(
name=self.name,
comments=extends_from.comments + self.comments,
pragmas=extends_from.pragmas + self.pragmas,
meta=self.meta,
filename=self.filename,
source_code="\n\n".join((extends_from.source_code, self.source_code)),
item=self.item,
extends=self.extends,
declarations=declarations,
actions=actions,
methods=methods,
properties=properties,
squashed=True,
)
[docs]
@dataclass
class InterfaceSummary(Summary):
"""Summary representation of an Interfae."""
name: str
source_code: str
item: tf.Interface
extends: Optional[str]
squashed: bool
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
methods: List[MethodSummary] = field(default_factory=list)
properties: List[PropertySummary] = field(default_factory=list)
# TwinCAT IDE doesn't allow for actions to be added to interfaces, it
# seems. Overlap with methods?
# actions: List[ActionSummary] = field(default_factory=list)
def __getitem__(
self, key: str
) -> Union[DeclarationSummary, MethodSummary, PropertySummary]:
if key in self.declarations:
return self.declarations[key]
for item in self.methods + self.properties:
if item.name == key:
return item
raise KeyError(key)
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
result = {}
for decl in self.declarations.values():
result.setdefault(decl.block, {})[decl.name] = decl
return result
[docs]
@classmethod
def from_interface(
cls,
itf: tf.Interface,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> InterfaceSummary:
if source_code is None:
source_code = str(itf)
summary = InterfaceSummary(
name=itf.name,
item=itf,
source_code=source_code,
filename=filename,
extends=itf.extends.name if itf.extends else None,
squashed=False,
**Summary.get_meta_kwargs(itf.meta),
)
for decl in itf.declarations:
summary.declarations.update(
DeclarationSummary.from_block(decl, parent=itf, filename=filename)
)
return summary
[docs]
def squash_base_extends(
self, interfaces: Dict[str, InterfaceSummary]
) -> InterfaceSummary:
"""Squash the "EXTENDS" INTERFACE into this one."""
if self.extends is None:
return self
extends_from = interfaces.get(str(self.extends), None)
if extends_from is None:
return self
if extends_from.extends:
extends_from = extends_from.squash_base_extends(interfaces)
declarations = dict(extends_from.declarations)
declarations.update(self.declarations)
methods = list(extends_from.methods) + self.methods
properties = list(extends_from.properties) + self.properties
return InterfaceSummary(
name=self.name,
comments=extends_from.comments + self.comments,
pragmas=extends_from.pragmas + self.pragmas,
meta=self.meta,
filename=self.filename,
source_code="\n\n".join((extends_from.source_code, self.source_code)),
item=self.item,
extends=self.extends,
declarations=declarations,
properties=properties,
methods=methods,
squashed=True,
)
[docs]
@dataclass
class DataTypeSummary(Summary):
"""Summary representation of a single data type."""
# Note: structures only for now.
name: str
item: tf.TypeDeclarationItem
source_code: str
type: str
extends: Optional[str]
squashed: bool = False
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
def __getitem__(self, key: str) -> DeclarationSummary:
return self.declarations[key]
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
return {
"STRUCT": self.declarations
}
[docs]
@classmethod
def from_data_type(
cls,
dtype: tf.TypeDeclarationItem,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> DataTypeSummary:
if source_code is None:
source_code = str(dtype)
if isinstance(dtype, tf.StructureTypeDeclaration):
extends = dtype.extends.name if dtype.extends else None
else:
extends = None
summary = cls(
name=dtype.name,
item=dtype,
extends=extends,
source_code=source_code,
type=type(dtype).__name__,
filename=filename,
squashed=False,
**Summary.get_meta_kwargs(dtype.meta),
)
if isinstance(dtype, tf.StructureTypeDeclaration):
for decl in dtype.declarations:
summary.declarations.update(
DeclarationSummary.from_declaration(
decl,
parent=dtype,
block_header="STRUCT",
filename=filename,
)
)
if isinstance(dtype, tf.UnionTypeDeclaration):
for decl in dtype.declarations:
summary.declarations.update(
DeclarationSummary.from_declaration(
decl,
parent=dtype,
block_header="UNION",
filename=filename,
)
)
return summary
[docs]
def squash_base_extends(
self, data_types: Dict[str, DataTypeSummary]
) -> DataTypeSummary:
"""Squash the "EXTENDS" function block into this one."""
if self.extends is None:
return self
extends_from = data_types.get(str(self.extends), None)
if extends_from is None:
return self
if extends_from.extends:
extends_from = extends_from.squash_base_extends(data_types)
declarations = dict(extends_from.declarations)
declarations.update(self.declarations)
return DataTypeSummary(
name=self.name,
type=self.type,
comments=extends_from.comments + self.comments,
pragmas=extends_from.pragmas + self.pragmas,
meta=self.meta,
filename=self.filename,
source_code="\n\n".join((extends_from.source_code, self.source_code)),
item=self.item,
extends=self.extends,
declarations=declarations,
squashed=True,
)
[docs]
@dataclass
class GlobalVariableSummary(Summary):
"""Summary representation of a VAR_GLOBAL block."""
name: str
item: tf.GlobalVariableDeclarations
source_code: str
type: str
qualified_only: bool = False
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
def __getitem__(self, key: str) -> DeclarationSummary:
return self.declarations[key]
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
return {
"VAR_GLOBAL": self.declarations
}
[docs]
@classmethod
def from_globals(
cls,
decls: tf.GlobalVariableDeclarations,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> GlobalVariableSummary:
if source_code is None:
source_code = str(decls)
summary = GlobalVariableSummary(
name=decls.name or "(unknown)",
item=decls,
source_code=source_code,
type=type(decls).__name__,
filename=filename,
qualified_only="qualified_only" in decls.attribute_pragmas,
**Summary.get_meta_kwargs(decls.meta),
)
for decl in decls.items:
summary.declarations.update(
**DeclarationSummary.from_global_variable(
decl,
parent=summary,
block_header="VAR_GLOBAL",
filename=filename,
)
)
return summary
[docs]
@dataclass
class ProgramSummary(Summary):
"""Summary representation of a single program."""
name: str
source_code: str
item: tf.Program
implementation: Optional[tf.StatementList] = None
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
actions: List[ActionSummary] = field(default_factory=list)
methods: List[MethodSummary] = field(default_factory=list)
properties: List[PropertySummary] = field(default_factory=list)
def __getitem__(self, key: str) -> DeclarationSummary:
if key in self.declarations:
return self.declarations[key]
for item in self.actions + self.methods + self.properties:
if item.name == key:
return item
raise KeyError(key)
@property
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
result = {}
for decl in self.declarations.values():
result.setdefault(decl.block, {})[decl.name] = decl
return result
[docs]
@classmethod
def from_program(
cls,
program: tf.Program,
source_code: Optional[str] = None,
filename: Optional[pathlib.Path] = None,
) -> ProgramSummary:
if source_code is None:
source_code = str(program)
summary = ProgramSummary(
name=program.name,
item=program,
source_code=source_code,
filename=filename,
**Summary.get_meta_kwargs(program.meta),
)
for decl in program.declarations:
summary.declarations.update(
DeclarationSummary.from_block(decl, parent=program, filename=filename)
)
return summary
[docs]
def path_to_file_and_line(path: List[Summary]) -> List[Tuple[pathlib.Path, int]]:
"""Get the file/line number context for the summary items."""
return [(part.filename, part.item.meta.line) for part in path]
TopLevelCodeSummaryType = Union[
FunctionSummary,
FunctionBlockSummary,
DataTypeSummary,
ProgramSummary,
InterfaceSummary,
GlobalVariableSummary,
]
NestedCodeSummaryType = Union[
DeclarationSummary,
MethodSummary,
PropertySummary,
ActionSummary,
PropertyGetSetSummary,
]
CodeSummaryType = Union[
TopLevelCodeSummaryType,
NestedCodeSummaryType,
]
[docs]
@dataclass
class CodeSummary:
"""Summary representation of a set of code - functions, function blocks, etc."""
functions: Dict[str, FunctionSummary] = field(default_factory=dict)
function_blocks: Dict[str, FunctionBlockSummary] = field(
default_factory=dict
)
data_types: Dict[str, DataTypeSummary] = field(default_factory=dict)
programs: Dict[str, ProgramSummary] = field(default_factory=dict)
globals: Dict[str, GlobalVariableSummary] = field(default_factory=dict)
interfaces: Dict[str, InterfaceSummary] = field(default_factory=dict)
def __str__(self) -> str:
attr_to_header = {
"data_types": "Data Types",
"globals": "Global Variable Declarations",
"interfaces": "Interface Declarations",
"functions": "Functions",
"function_blocks": "Function Blocks",
"programs": "Programs",
}
summary_text = []
for attr, header in attr_to_header.items():
name_to_obj = getattr(self, attr)
if name_to_obj:
summary_text.extend(
[
header,
"-" * len(header),
]
)
for name, obj in name_to_obj.items():
obj_info = "\n".join(
(
name,
"=" * len(name),
str(obj)
)
)
summary_text.append(textwrap.indent(obj_info, " " * 4))
return "\n".join(summary_text)
[docs]
def find(self, name: str) -> Optional[CodeSummaryType]:
"""Find a declaration or other item by its qualified name."""
path = self.find_path(name, allow_partial=False)
return path[-1] if path else None
[docs]
def find_path(
self,
name: str,
allow_partial: bool = False,
) -> Optional[List[CodeSummaryType]]:
"""
Given a qualified variable name, find the path of CodeSummary objects top-down.
For example, a variable declared in a function block would return a
list containing the FunctionBlockSummary and then the
DeclarationSummary.
Parameters
----------
name : str
The qualified ("dotted") variable name to find.
allow_partial : bool, optional
If an attribute is missing along the way, return the partial
path that was found.
Returns
-------
list of CodeSummaryType or None
The full path to the given object.
"""
parts = collections.deque(name.split("."))
if len(parts) <= 1:
item = self.get_item_by_name(name)
if item is None:
return None
return [item]
variable_name = parts.pop()
parent = None
path = []
while parts:
part = parts.popleft()
if "[" in part: # ]
part = part.split("[")[0] # ]
try:
if parent is None:
parent = self.get_item_by_name(part)
path.append(parent)
else:
part_obj = parent[part]
path.append(part_obj)
part_type = str(part_obj.base_type)
parent = self.get_item_by_name(part_type)
except KeyError:
if allow_partial:
return path
return
if parent is None:
return
try:
path.append(parent[variable_name])
except KeyError:
if not allow_partial:
return None
return path
[docs]
def find_code_object_by_dotted_name(self, name: str) -> Optional[CodeSummaryType]:
"""
Given a qualified code object name, find its Summary object(s).
This works to find CodeSummary objects such as::
FB_Block.ActionName
FB_Block.PropertyName.get
FB_Block.PropertyName.set
"""
parts = Identifier.from_string(name).parts
obj = self.get_item_by_name(parts[0])
if len(parts) == 1:
return obj
if obj is None:
raise ValueError(f"No object by the name of {parts[0]} exists")
for remainder in parts[1:]:
next_obj = obj[remainder]
if next_obj is None:
raise ValueError(f"{name}: {obj} has no attribute {remainder!r}")
obj = next_obj
return obj
[docs]
def get_all_items_by_name(
self,
name: str,
) -> Generator[TopLevelCodeSummaryType, None, None]:
"""Get any code item (function, data type, global variable, etc.) by name."""
for dct in (
self.globals,
self.programs,
self.functions,
self.function_blocks,
self.data_types,
):
# Very inefficient, be warned
try:
yield dct[name]
except KeyError:
...
[docs]
def get_item_by_name(self, name: str) -> Optional[TopLevelCodeSummaryType]:
"""
Get a single code item (function, data type, global variable, etc.) by name.
Does not handle scenarios where names are shadowed by other
declarations. The first one found will take precedence.
"""
try:
return next(self.get_all_items_by_name(name))
except StopIteration:
return None
def __getitem__(self, name: str) -> TopLevelCodeSummaryType:
item = self.get_item_by_name(name)
if item is None:
raise KeyError(f"{name!r} is not a top-level code object name")
return item
[docs]
def append(self, other: CodeSummary, namespace: Optional[str] = None):
"""
In-place add code summary information from another instance.
New entries take precedence over old ones.
"""
self.functions.update(other.functions)
self.function_blocks.update(other.function_blocks)
self.data_types.update(other.data_types)
self.globals.update(other.globals)
self.programs.update(other.programs)
self.interfaces.update(other.interfaces)
if namespace:
# LCLS_General.GVL_Logger and GVL_Logger are equally valid
for name, item in other.functions.items():
self.functions[f"{namespace}.{name}"] = item
for name, item in other.function_blocks.items():
self.function_blocks[f"{namespace}.{name}"] = item
for name, item in other.data_types.items():
self.data_types[f"{namespace}.{name}"] = item
for name, item in other.globals.items():
self.globals[f"{namespace}.{name}"] = item
# for name, item in other.programs.items():
# self.programs[f"{namespace}.{name}"] = item
self.squash()
[docs]
@staticmethod
def from_parse_results(
all_parsed_items: Union[ParseResult, list[ParseResult]],
squash: bool = True,
) -> CodeSummary:
if isinstance(all_parsed_items, ParseResult):
all_parsed_items = [all_parsed_items]
result = CodeSummary()
def get_code_by_meta(parsed: ParseResult, meta: Optional[tf.Meta]) -> str:
if meta is None or meta.line is None or meta.end_line is None:
return ""
transformed = parsed.transform()
return "\n".join(
transformed.range_from_file_lines(meta.line, meta.end_line)
)
def add_implementation(parsed: ParseResult, impl: tf.StatementList):
assert parsed.identifier is not None
identifier = Identifier.from_string(parsed.identifier)
assert identifier.decl_impl == "implementation"
match = result.find_code_object_by_dotted_name(identifier.dotted_name)
if match is None:
raise RuntimeError(
f"Implementation without previous declaration? "
f"{parsed.filename} {parsed.identifier}"
)
# if len(matches) > 1:
# raise RuntimeError(
# f"Multiple matches for implementation? "
# f"{parsed.filename} {parsed.identifier}"
# )
if match.implementation is not None:
raise RuntimeError(
f"Implementation specified twice for {parsed.filename} "
f"{parsed.identifier}"
)
if isinstance(match, PropertyGetSetSummary):
...
match.implementation = impl
context = []
def clear_context():
context.clear()
def new_context(summary: Summary):
context[:] = [summary]
def push_context(summary: Summary):
context.append(summary)
def get_pou_context() -> Union[
ProgramSummary, FunctionBlockSummary, InterfaceSummary
]:
for item in reversed(context):
if isinstance(
item, (ProgramSummary, FunctionBlockSummary, InterfaceSummary)
):
return item
raise ValueError(
"Expected to parse a POU prior to this but none were in the context "
"list. Code summaries of PROPERTY objects, for example, require "
"that a FUNCTION_BLOCK, INTERFACE, or PROGRAM be parsed previously."
)
for parsed in all_parsed_items:
transformed = parsed.transform()
identifier = Identifier.from_string(parsed.identifier) if parsed.identifier else None
for item in transformed.items:
if isinstance(item, tf.FunctionBlock):
summary = FunctionBlockSummary.from_function_block(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
result.function_blocks[item.name] = summary
new_context(summary)
elif isinstance(item, tf.Function):
summary = FunctionSummary.from_function(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
result.functions[item.name] = summary
new_context(summary)
elif isinstance(item, tf.DataTypeDeclaration):
if isinstance(
item.declaration,
(tf.StructureTypeDeclaration, tf.UnionTypeDeclaration)
):
summary = DataTypeSummary.from_data_type(
item.declaration,
source_code=get_code_by_meta(parsed, item.declaration.meta),
filename=parsed.filename,
)
result.data_types[item.declaration.name] = summary
clear_context()
elif isinstance(item, tf.Method):
pou = get_pou_context()
summary = MethodSummary.from_method(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
pou.methods.append(summary)
push_context(summary)
elif isinstance(item, tf.Action):
pou = get_pou_context()
summary = ActionSummary.from_action(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
pou.actions.append(summary)
push_context(summary)
elif isinstance(item, tf.Property):
pou = get_pou_context()
summary = PropertySummary.from_property(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
pou.properties.append(summary)
push_context(summary)
elif isinstance(item, tf.GlobalVariableDeclarations):
clear_context()
summary = GlobalVariableSummary.from_globals(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
result.globals[item.name] = summary
# for global_var in summary.declarations.values():
# if not qualified_only:
# result.globals[global_var.name] = summary
# result.globals[global_var.qualified_name] = summary
elif isinstance(item, tf.Program):
summary = ProgramSummary.from_program(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
result.programs[item.name] = summary
new_context(summary)
elif isinstance(item, tf.Interface):
summary = InterfaceSummary.from_interface(
item,
source_code=get_code_by_meta(parsed, item.meta),
filename=parsed.filename,
)
result.interfaces[item.name] = summary
new_context(summary)
elif isinstance(item, tf.StatementList):
if parsed.item.type != SourceType.action:
add_implementation(parsed, item)
else:
assert identifier is not None
# Special-case: actions only have implementations
parent_identifier = Identifier(parts=identifier.parts[:-1])
parent, = result.find_path(parent_identifier.dotted_name)
assert isinstance(parent, (FunctionBlockSummary, ProgramSummary))
action = ActionSummary.from_statement_list(
name=identifier.parts[-1],
statements=item,
source_code=parsed.source_code, # TODO above?
filename=parsed.filename,
)
parent.actions.append(action)
else:
raise ValueError(type(item))
logger.warning("Unhandled: %s", type(item))
if squash:
result.squash()
return result
[docs]
def squash(self) -> None:
"""Squash derived interfaces/etc to include base summaries."""
for name, item in list(self.function_blocks.items()):
if item.extends and not item.squashed:
self.function_blocks[name] = item.squash_base_extends(
self.function_blocks
)
for name, item in list(self.data_types.items()):
if item.extends and not item.squashed:
self.data_types[name] = item.squash_base_extends(
self.data_types
)
for name, item in list(self.interfaces.items()):
if item.extends and not item.squashed:
self.interfaces[name] = item.squash_base_extends(
self.interfaces
)
[docs]
@dataclass
class LinkableItems:
"""A summary of linkable (located) declarations."""
input: List[DeclarationSummary] = field(default_factory=list)
output: List[DeclarationSummary] = field(default_factory=list)
memory: List[DeclarationSummary] = field(default_factory=list)
[docs]
def get_linkable_declarations(
declarations: Iterable[DeclarationSummary],
) -> LinkableItems:
"""
Get all located/linkable declarations.
"""
linkable = LinkableItems()
for decl in declarations:
linkable_list = getattr(linkable, decl.location_type or "", None)
if linkable_list is not None:
linkable_list.append(decl)
return linkable