Skip to content

Commit

Permalink
Finished GetCurrentByName and UpdateSourceMap: GetCurrentByName now s…
Browse files Browse the repository at this point in the history
…tores the MethodMap (line numbers for each method), and executable lines got a bug fix. UpdateSourceMap now saves mapped line numbers from .py to .cls
  • Loading branch information
isc-cge committed Jul 1, 2024
1 parent d0ce282 commit e9860ce
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 45 deletions.
143 changes: 105 additions & 38 deletions cls/TestCoverage/Data/CodeUnit.cls
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Property ExecutableLines As TestCoverage.DataType.Bitstring;

/// For classes, map of method names in the code to their associated line numbers
/// For routines, map of labels to associated line numbers
/// For python, map of method names to a $lb(starting line number, ending line number)
Property MethodMap As array Of %Integer;

/// For classes, map of line numbers in code to associated method names
Expand Down Expand Up @@ -98,7 +99,7 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri
}

Set $Namespace = pSourceNamespace

If (tType = "CLS") {
Do ##class(TestCoverage.Utils).GetClassLineExecutableFlags(tName,.tCodeArray,.tExecutableFlags)
} ElseIf ((tType = "INT") || (tType = "MAC")) {
Expand All @@ -107,13 +108,14 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri
Do ##class(TestCoverage.Utils).CodeArrayToList(.tCodeArray, .pDocumentText)
Set tExecutableFlagsPyList = ##class(TestCoverage.Utils).GetPythonLineExecutableFlags(pDocumentText)
Kill tExecutableFlags
for i=1:1:tExecutableFlagsPyList."__len__()"-1 {
for i=1:1:tExecutableFlagsPyList."__len__"()-1 {
set tExecutableFlags(i) = tExecutableFlagsPyList."__getitem__"(i)
}
}
Else {
return $$$ERROR($$$GeneralError,"File type not supported")
}

Set $Namespace = tOriginalNamespace
Set pCodeUnit = ..%New()
Set pCodeUnit.Name = tName
Expand All @@ -130,46 +132,80 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri
Set pCodeUnit.Generated = ($$$comClassKeyGet(tName,$$$cCLASSgeneratedby) '= "")
}

Set tMethod = ""
Set tMethodSignature = ""
Set tMethodMask = ""
For tLineNumber=1:1:$Get(tCodeArray,0) {
Set tLine = tCodeArray(tLineNumber)
Do pCodeUnit.Lines.Insert(tLine)

If (tType = "CLS") {
// Extract line offset of methods in classes
Set tStart = $Piece(tLine," ")
If (tStart = "ClassMethod") || (tStart = "Method") {
Set tMethod = $Piece($Piece(tLine,"(")," ",2)
Set tMethodSignature = tLine
Do pCodeUnit.MethodMap.SetAt(tLineNumber,tMethod)
Do pCodeUnit.LineToMethodMap.SetAt(tMethod,tLineNumber)
} ElseIf ($Extract(tStart) = "{") {
// Ignore the opening bracket for a method.
} ElseIf ($Extract(tStart) = "}") && (tMethod '= "") {
// End of method. Add method subunit to class.
Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New()
Set tSubUnit.Name = tMethod
Set tSubUnit.DisplaySignature = tMethodSignature
Set tSubUnit.Mask = tMethodMask
Do pCodeUnit.SubUnits.Insert(tSubUnit)
Set tMethod = ""
Set tMethodSignature = ""
Set tMethodMask = ""
} ElseIf (tMethod '= "") {
Set $Bit(tMethodMask,tLineNumber) = 1

If (tType = "PY") {
Set ClassName = $Piece(tName,".", *)
Set tMethodInfo = ##class(TestCoverage.Utils).GetPythonMethodMapping(pDocumentText, ClassName)
Set tLineToMethodInfo = tMethodInfo."__getitem__"(0)
Set tMethodMapInfo = tMethodInfo."__getitem__"(1)
for i=1:1:$listlength(pDocumentText) {
Set tMethod = tLineToMethodInfo."__getitem__"(i)
Do pCodeUnit.LineToMethodMap.SetAt(tMethod,i)
}
Set iterator = tMethodMapInfo."__iter__"()
for i=1:1:tMethodMapInfo."__len__"() {
Set tMethod = iterator."__next__"()
Set tStartEnd = tMethodMapInfo."__getitem__"(tMethod)
Set tStartLine = tStartEnd."__getitem__"(0)
Set tEndLine = tStartEnd."__getitem__"(1)
Do pCodeUnit.MethodMap.SetAt($lb(tStartLine, tEndLine),tMethod)

Set tMethodMask = ""
for j = tStartLine:1:tEndLine {
Set $Bit(tMethodMask,j) = 1
}
} Else {
// Extract line offset of labels in routines
If ($ZStrip($Extract(tLine),"*PWC") '= "") {
Set tLabel = $Piece($Piece(tLine," "),"(")
Do pCodeUnit.MethodMap.SetAt(tLineNumber,tLabel)
Do pCodeUnit.LineToMethodMap.SetAt(tLabel,tLineNumber)
Set tMethodSignature = $list(pDocumentText, tStartLine)
Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New()
Set tSubUnit.Name = tMethod
Set tSubUnit.DisplaySignature = tMethodSignature
Set tSubUnit.Mask = tMethodMask
Do pCodeUnit.SubUnits.Insert(tSubUnit)
}
}
Else {
Set tMethod = ""
Set tMethodSignature = ""
Set tMethodMask = ""
For tLineNumber=1:1:$Get(tCodeArray,0) {
Set tLine = tCodeArray(tLineNumber)
Do pCodeUnit.Lines.Insert(tLine)

If (tType = "CLS") {
// Extract line offset of methods in classes
Set tStart = $Piece(tLine," ")
If (tStart = "ClassMethod") || (tStart = "Method") {
Set tMethod = $Piece($Piece(tLine,"(")," ",2)
Set tMethodSignature = tLine
Do pCodeUnit.MethodMap.SetAt(tLineNumber,tMethod)
Do pCodeUnit.LineToMethodMap.SetAt(tMethod,tLineNumber)
} ElseIf ($Extract(tStart) = "{") {
// Ignore the opening bracket for a method.
} ElseIf ($Extract(tStart) = "}") && (tMethod '= "") {
// End of method. Add method subunit to class.
Set tSubUnit = ##class(TestCoverage.Data.CodeSubUnit.Method).%New()
Set tSubUnit.Name = tMethod
Set tSubUnit.DisplaySignature = tMethodSignature
Set tSubUnit.Mask = tMethodMask
Do pCodeUnit.SubUnits.Insert(tSubUnit)
Set tMethod = ""
Set tMethodSignature = ""
Set tMethodMask = ""
} ElseIf (tMethod '= "") {
Set $Bit(tMethodMask,tLineNumber) = 1
}
}
Else {
// Extract line offset of labels in routines
If ($ZStrip($Extract(tLine),"*PWC") '= "") {
Set tLabel = $Piece($Piece(tLine," "),"(")
Do pCodeUnit.MethodMap.SetAt(tLineNumber,tLabel)
Do pCodeUnit.LineToMethodMap.SetAt(tLabel,tLineNumber)
}
}
}
}


Set tSC = pCodeUnit.%Save()
If $$$ISERR(tSC) && $System.Status.Equals(tSC,$$$ERRORCODE($$$IDKeyNotUnique)) {
// Some other process beat us to it.
Expand Down Expand Up @@ -197,6 +233,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status
{
Set tSC = $$$OK
Try {

// First, build local array (tMap) of all maps from the .INT file to other files.
If (..Type = "INT") {
For tLineNumber=1:1:..Lines.Count() {
Expand Down Expand Up @@ -235,6 +272,36 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status
}
}
}
If (..Type = "PY") {

set tClass = ..Name
Set tSourceUnits(tClass_".CLS") = ""
$$$ThrowOnError(..GetCurrentByName(tClass _ ".CLS", pSourceNamespace, .pCLSCodeUnit, .pCLSCache))
// we'll do the mappings from the .py to the .cls direction, so that we don't iterate over objectscript lines
Set tMethod = ""
Do ..MethodMap.GetNext(.tMethod)
while (tMethod '= "")
{
Set tCLSMethodNum = pCLSCodeUnit.MethodMap.GetAt(tMethod)
Set $lb(tMethodStart, tMethodEnd) = ..MethodMap.GetAt(tMethod)
Set tMethodName = tMethod
Set tFullMap(tMethodStart) = $lb("CLS", tClass,tMethodName, -1, -1) ; -1 because the class
; definition doesn't have the +1 offset from the {

Do ..MethodMap.GetNext(.tMethod)
For i = tMethodStart+1:1:tMethodEnd {
Set tClassLineNum = i-tMethodStart
Set tFullMap(i) = $lb("CLS", tClass,tMethodName, tClassLineNum, tClassLineNum)

// extra check to make sure that the lines we're mapping between are the same as expected
Set tClassLineCode = $zstrip(pCLSCodeUnit.Lines.GetAt(tCLSMethodNum + tClassLineNum + 1), "<>W")
Set tPyLineCode = $zstrip(..Lines.GetAt(i), "<>W")
if (tPyLineCode '= tClassLineCode) {
Set tSC = $$$ERROR($$$GeneralError,"Compiled .py code doesn't match .CLS python code ")
}
}
}
}

// If we are a generator .INT file, ensure that we have source for the original class populated.
// In such files, the second line looks like (for example):
Expand Down Expand Up @@ -280,7 +347,7 @@ Method UpdateSourceMap(pSourceNamespace As %String, ByRef pCache) As %Status
Set tCodeUnits(tCodeUnit.Type,tCodeUnit.Name) = tCodeUnit
}

// Create CodeUnitMap data based on .INT->.CLS mapping.
// Create CodeUnitMap data based on .INT / .py ->.CLS mapping.
Set tFromHash = ..Hash
Set tLineNumber = ""
For {
Expand Down
56 changes: 49 additions & 7 deletions cls/TestCoverage/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,61 @@ ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List)
quit
}

ClassMethod GetPythonMethodMapping(pDocumentText, ClassName) [ Language = python ]
{
import iris
import ast
source_lines = iris.cls('%SYS.Python').ToList(pDocumentText)
source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code

class_name = ClassName
source = ''.join(source_lines)
tree = ast.parse(source)
line_function_map = [None] * (len(source_lines)+2)
method_map = {}

class FunctionMapper(ast.NodeVisitor):
def __init__(self):
self.current_class = None
self.current_function = None
self.outermost_function = None

def visit_ClassDef(self, node):
prev_class = self.current_class
self.current_class = node.name
self.generic_visit(node)
self.current_class = prev_class

def visit_FunctionDef(self, node):
if self.outermost_function is None:
self.outermost_function = node.name
method_map[node.name] = (node.lineno-1, node.end_lineno-1)

self.current_function = node.name
for lineno in range(node.lineno, node.end_lineno + 1):
line_function_map[lineno-1] = self.outermost_function

self.generic_visit(node)
self.current_function = None
if self.outermost_function == node.name:
self.outermost_function = None

tree_with_line_numbers = ast.increment_lineno(tree, n=1)
for node in ast.walk(tree_with_line_numbers):
if isinstance(node, ast.FunctionDef):
node.end_lineno = node.body[-1].end_lineno

FunctionMapper().visit(tree)
return (line_function_map, method_map)
}

/// returns a python list with a 1 or 0 for subscript i indicating if line i is executable or not
ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ]
{
import iris
import ast
source_lines = iris.cls('%SYS.Python').ToList(pDocumentText)
source_lines = [line + "\n" for line in source_lines] # contains a list of each line of the source code
print(source_lines)
glob = iris.gref('IRIS.TEMPCG')

# create the abstract syntax tree for the code, and walk through it, getting each line of code in its context
source = ''.join(source_lines)
Expand All @@ -426,13 +472,9 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ]
if isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)):
decorators = [element.id for element in node.decorator_list]
num_decorators = len(decorators)
# print(f"{node.lineno=}")
for i, element in enumerate(decorators):
# print(f"{element=}")
conjectured_line = (node.lineno-1)-num_decorators+i # change this back if the line numbers aren't 0 indexed
# print(f"{source_lines[conjectured_line]=}")
if "@" + element in source_lines[conjectured_line]:
#print(f"added {element=}")
executable_lines.add(conjectured_line+1) # because back to 1-indexing
executable_lines.add(node.lineno)
elif isinstance(node, (ast.Call,
Expand All @@ -458,7 +500,7 @@ ClassMethod GetPythonLineExecutableFlags(pDocumentText) [ Language = python ]
for i, line in enumerate(source_lines, start=1):
is_exec = output[i]
print(f"{i:2d} {'*' if is_exec else ' '} {line.rstrip()}")
print_executable_lines()
# print_executable_lines()
return output
}

Expand Down

0 comments on commit e9860ce

Please sign in to comment.