Skip to content

Commit

Permalink
Added code to find what lines in a python file are executable, and se…
Browse files Browse the repository at this point in the history
…t it in the CodeUnit
  • Loading branch information
isc-cge committed Jun 28, 2024
1 parent d591894 commit d0ce282
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 4 deletions.
14 changes: 12 additions & 2 deletions cls/TestCoverage/Data/CodeUnit.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 71 additions & 2 deletions cls/TestCoverage/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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 <var>pDocumentText</var> as an integer-subscripted array of lines,
/// returns an array (<var>pExecutableFlags</var>) subscripted by line with boolean flags indicating whether the corresponding line is executable.
ClassMethod GetRoutineLineExecutableFlags(ByRef pDocumentText, Output pExecutableFlags)
Expand All @@ -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)

Expand Down Expand Up @@ -540,4 +610,3 @@ ClassMethod LineByLineMonitorResultClose(ByRef qHandle As %Binary) As %Status [
}

}

0 comments on commit d0ce282

Please sign in to comment.