diff --git a/cls/TestCoverage/Data/CodeUnit.cls b/cls/TestCoverage/Data/CodeUnit.cls index e03e143..a90bea9 100644 --- a/cls/TestCoverage/Data/CodeUnit.cls +++ b/cls/TestCoverage/Data/CodeUnit.cls @@ -98,12 +98,22 @@ ClassMethod GetCurrentByName(pInternalName As %String, pSourceNamespace As %Stri } Set $Namespace = pSourceNamespace + If (tType = "CLS") { Do ##class(TestCoverage.Utils).GetClassLineExecutableFlags(tName,.tCodeArray,.tExecutableFlags) - } Else { + } ElseIf ((tType = "INT") || (tType = "MAC")) { Do ##class(TestCoverage.Utils).GetRoutineLineExecutableFlags(.tCodeArray,.tExecutableFlags) + } ElseIf (tType="PY") { + Do ##class(TestCoverage.Utils).CodeArrayToList(.tCodeArray, .pDocumentText) + Set tExecutableFlagsPyList = ##class(TestCoverage.Utils).GetPythonLineExecutableFlags(pDocumentText) + Kill tExecutableFlags + 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 diff --git a/cls/TestCoverage/Utils.cls b/cls/TestCoverage/Utils.cls index 2cd8662..cd6d5b0 100644 --- a/cls/TestCoverage/Utils.cls +++ b/cls/TestCoverage/Utils.cls @@ -391,6 +391,77 @@ ClassMethod GetClassLineExecutableFlags(pClassName As %String, ByRef pDocumentTe } } +ClassMethod CodeArrayToList(ByRef pCodeArray, Output pDocumentText As %List) +{ + set pDocumentText = $lb() + for i=1:1:pCodeArray(0) { + set $list(pDocumentText, i) = pCodeArray(i) + } + quit +} + +/// 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) + tree = ast.parse(source) + executable_lines = set() # stores the 1-indexed line numbers of the executable lines + + class ExecutableLineVisitor(ast.NodeVisitor): + def __init__(self): + self.function_depth = 0 + + def visit(self, node): + if hasattr(node, 'lineno'): + + # decorators for functions and class definitions are executable + 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, + ast.Return, ast.Assign, ast.AugAssign, ast.AnnAssign, + ast.For, ast.AsyncFor, ast.While, ast.If, ast.With, + ast.AsyncWith, ast.Raise, ast.Try, ast.Assert, + ast.Import, ast.ImportFrom, ast.Pass, + ast.Break, ast.Continue, ast.Delete, ast.Yield, + ast.YieldFrom, ast.Await, ast.Nonlocal)): # all executable + executable_lines.add(node.lineno) + elif isinstance(node, ast.ExceptHandler): # except (but not finally) is executable + executable_lines.add(node.lineno) + elif isinstance(node, ast.Expr) and not isinstance(node.value, ast.Constant): # expressions that aren't docstrings are executable + executable_lines.add(node.lineno) + self.generic_visit(node) + ExecutableLineVisitor().visit(tree) + + output = [0] * (len(source_lines)+1) + for line in executable_lines: + output[line] = 1 + output[1] = 0 # manually set the class definition to be not executable + def print_executable_lines(): + 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() + return output +} + /// For a routine (.MAC/.INT) with code in pDocumentText as an integer-subscripted array of lines, /// returns an array (pExecutableFlags) subscripted by line with boolean flags indicating whether the corresponding line is executable. ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutableFlags) @@ -400,7 +471,6 @@ ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutabl For tDocLine=1:1:$Get(pDocumentText) { Do tSourceStream.WriteLine(pDocumentText(tDocLine)) } - Set tSC = ##class(%Library.SyntaxColorReader).FromCode(tSourceStream,"COS","A",.tSCReader) $$$ThrowOnError(tSC) @@ -540,4 +610,3 @@ ClassMethod LineByLineMonitorResultClose(ByRef qHandle As %Binary) As %Status [ } } -