Skip to content

Commit

Permalink
Add support for Python 3.9 (#633)
Browse files Browse the repository at this point in the history
To maximize adoption, support the full range of Python versions that are
not end-of-life. So this change adds the final needed of Python 3.9.

This does affect some of the code:
* Can no longer use match statements
* Use typing.Optional instead of <type> | None
* Getting third party plugin via metadata distributions is disabled
* Modified how extensions are found by group and loaded

Signed-off-by: Eric Brown <[email protected]>
  • Loading branch information
ericwb authored Oct 10, 2024
1 parent e810dd5 commit d4135e3
Show file tree
Hide file tree
Showing 69 changed files with 478 additions and 366 deletions.
24 changes: 12 additions & 12 deletions precli/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,10 @@ def setup_arg_parser():
help="quiet mode, display less output",
)
extensions = ""
for dist in metadata.distributions():
if dist.name.startswith("precli-"):
extensions += f" {dist.name} {dist.version}\n"
if sys.version_info >= (3, 10):
for dist in metadata.distributions():
if dist.name.startswith("precli-"):
extensions += f" {dist.name} {dist.version}\n"
python_ver = sys.version.replace("\n", "")
parser.add_argument(
"--version",
Expand Down Expand Up @@ -289,15 +290,14 @@ def discover_files(targets: list[str], recursive: bool):


def create_gist(file, renderer: str):
match renderer:
case "json":
filename = "results.json"
case "plain":
filename = "results.txt"
case "markdown":
filename = "results.md"
case "detailed":
filename = "results.txt"
if renderer == "json":
filename = "results.json"
elif renderer == "plain":
filename = "results.txt"
elif renderer == "markdown":
filename = "results.md"
elif renderer == "detailed":
filename = "results.txt"

with open(file.name, encoding="utf-8") as f:
file_content = f.read()
Expand Down
17 changes: 14 additions & 3 deletions precli/core/loader.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
# Copyright 2024 Secure Sauce LLC
import sys
from importlib.metadata import entry_points


def load_extension(group: str, name: str = ""):
if not name:
extensions = {}

for entry_point in entry_points(group=group):
if sys.version_info >= (3, 10):
eps = entry_points(group=group)
else:
eps = entry_points()[group]
for entry_point in eps:
extension = entry_point.load()()
extensions[entry_point.name] = extension

return extensions
else:
(entry_point,) = entry_points(group=group, name=name)
return entry_point.load()
if sys.version_info >= (3, 10):
(entry_point,) = entry_points(group=group, name=name)
return entry_point.load()
else:
eps = entry_points()[group]
for entry_point in eps:
if entry_point.name == name:
return entry_point.load()
46 changes: 22 additions & 24 deletions precli/core/redos.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,17 @@ def from_not_literal(cls, not_literal: tuple):
def _parse_in_nodes(nodes: tuple):
results = []
for node_type, args in nodes:
match node_type:
case constants.LITERAL:
results.append(CR(cr_min=args, cr_max=args))
case constants.RANGE:
results.append(CR(cr_min=args[0], cr_max=args[1]))
case constants.CATEGORY:
for c, r in CATEGORY_TO_RANGE.items():
if args is c:
results.extend(
CR(cr_min=r_min, cr_max=r_max)
for r_min, r_max in r
)
if node_type == constants.LITERAL:
results.append(CR(cr_min=args, cr_max=args))
elif node_type == constants.RANGE:
results.append(CR(cr_min=args[0], cr_max=args[1]))
elif node_type == constants.CATEGORY:
for c, r in CATEGORY_TO_RANGE.items():
if args is c:
results.extend(
CR(cr_min=r_min, cr_max=r_max)
for r_min, r_max in r
)

return results

Expand All @@ -160,18 +159,17 @@ def from_not_in(cls, not_in: tuple):

@classmethod
def from_op_node(cls, node: OpNode):
match node.op:
case constants.ANY:
return cls.from_any(node.args)
case constants.LITERAL:
return cls.from_literal(node.args)
case constants.NOT_LITERAL:
return cls.from_not_literal(node.args)
case constants.IN:
if node.args and node.args[0] == (constants.NEGATE, None):
return cls.from_not_in(node.args)
else:
return cls.from_in(node.args)
if node.op == constants.ANY:
return cls.from_any(node.args)
elif node.op == constants.LITERAL:
return cls.from_literal(node.args)
elif node.op == constants.NOT_LITERAL:
return cls.from_not_literal(node.args)
elif node.op == constants.IN:
if node.args and node.args[0] == (constants.NEGATE, None):
return cls.from_not_in(node.args)
else:
return cls.from_in(node.args)

# Unsupported OpNode
return None
Expand Down
3 changes: 2 additions & 1 deletion precli/core/symtab.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2024 Secure Sauce LLC
import sys
from typing import Optional

if sys.version_info >= (3, 11):
from typing import Self
Expand All @@ -18,7 +19,7 @@ def __init__(self, name: str, parent=None):
def name(self) -> str:
return self._name

def parent(self) -> Self | None:
def parent(self) -> Optional[Self]:
return self._parent

def put(self, name: str, type: str, value: str) -> None:
Expand Down
11 changes: 8 additions & 3 deletions precli/parsers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2024 Secure Sauce LLC
import importlib
import sys
from abc import ABC
from abc import abstractmethod
from importlib.metadata import entry_points
Expand Down Expand Up @@ -35,7 +36,11 @@ def __init__(self, lang: str):
self.rules = {}
self.wildcards = {}

discovered_rules = entry_points(group=f"precli.rules.{lang}")
if sys.version_info >= (3, 10):
discovered_rules = entry_points(group=f"precli.rules.{lang}")
else:
eps = entry_points()
discovered_rules = eps[f"precli.rules.{lang}"]
for rule in discovered_rules:
self.rules[rule.name] = rule.load()(rule.name)

Expand All @@ -46,7 +51,7 @@ def __init__(self, lang: str):
else:
self.wildcards[k] = v

def child_by_type(self, type: str) -> Node | None:
def child_by_type(self, type: str) -> Optional[Node]:
# Return first child with type as specified
child = list(filter(lambda x: x.type == type, self.named_children))
return child[0] if child else None
Expand Down Expand Up @@ -217,7 +222,7 @@ def visit_ERROR(self, nodes: list[Node]):
),
)

def child_by_type(self, node: Node, type: str) -> Node | None:
def child_by_type(self, node: Node, type: str) -> Optional[Node]:
# Return first child with type as specified
child = list(filter(lambda x: x.type == type, node.named_children))
return child[0] if child else None
Expand Down
119 changes: 59 additions & 60 deletions precli/parsers/go.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2024 Secure Sauce LLC
import ast
import re
from typing import Optional

from tree_sitter import Node

Expand Down Expand Up @@ -53,33 +54,32 @@ def visit_import_declaration(self, nodes: list[Node]):
def import_spec(self, nodes: list[Node]):
imports = {}

match nodes[0].type:
case NodeTypes.INTERPRETED_STRING_LITERAL:
# import "fmt"
package = ast.literal_eval(nodes[0].string)
default_package = package.split("/")[-1]
imports[default_package] = package

case NodeTypes.PACKAGE_IDENTIFIER:
# import fm "fmt"
# Can use fm.Println instead of fmt.Println
if nodes[1].type == NodeTypes.INTERPRETED_STRING_LITERAL:
alias = nodes[0].string
package = ast.literal_eval(nodes[1].string)
imports[alias] = package

case NodeTypes.DOT:
# import . "fmt"
# Can just call Println instead of fmt.Println
# TODO: similar to Python wildcard imports
pass

case NodeTypes.BLANK_IDENTIFIER:
# import _ "some/driver"
# The driver package is imported, and its init function is
# executed, but no access any of its other functions or
# variables directly.
pass
if nodes[0].type == NodeTypes.INTERPRETED_STRING_LITERAL:
# import "fmt"
package = ast.literal_eval(nodes[0].string)
default_package = package.split("/")[-1]
imports[default_package] = package

elif nodes[0].type == NodeTypes.PACKAGE_IDENTIFIER:
# import fm "fmt"
# Can use fm.Println instead of fmt.Println
if nodes[1].type == NodeTypes.INTERPRETED_STRING_LITERAL:
alias = nodes[0].string
package = ast.literal_eval(nodes[1].string)
imports[alias] = package

elif nodes[0].type == NodeTypes.DOT:
# import . "fmt"
# Can just call Println instead of fmt.Println
# TODO: similar to Python wildcard imports
pass

elif nodes[0].type == NodeTypes.BLANK_IDENTIFIER:
# import _ "some/driver"
# The driver package is imported, and its init function is
# executed, but no access any of its other functions or
# variables directly.
pass

return imports

Expand All @@ -90,7 +90,7 @@ def visit_function_declaration(self, nodes: list[Node]):
self.visit(nodes)
self.current_symtab = self.current_symtab.parent()

def _get_var_node(self, node: Node) -> Node | None:
def _get_var_node(self, node: Node) -> Optional[Node]:
if (
len(node.named_children) >= 2
and node.named_children[0].type
Expand All @@ -101,7 +101,7 @@ def _get_var_node(self, node: Node) -> Node | None:
elif node.type == NodeTypes.ATTRIBUTE:
return self._get_var_node(node.named_children[0])

def _get_func_ident(self, node: Node) -> Node | None:
def _get_func_ident(self, node: Node) -> Optional[Node]:
# TODO(ericwb): does this function fail with nested calls?
if node.type == NodeTypes.ATTRIBUTE:
return self._get_func_ident(node.named_children[1])
Expand Down Expand Up @@ -152,7 +152,7 @@ def get_func_args(self, node: Node) -> list:

return args

def get_qual_name(self, node: Node) -> Symbol | None:
def get_qual_name(self, node: Node) -> Optional[Symbol]:
nodetext = node.string
symbol = self.current_symtab.get(nodetext)

Expand Down Expand Up @@ -181,36 +181,35 @@ def resolve(self, node: Node, default=None):
default = default.string

try:
match node.type:
case NodeTypes.SELECTOR_EXPRESSION:
nodetext = node.string
symbol = self.get_qual_name(node)
if symbol is not None:
value = self.join_symbol(nodetext, symbol)
case NodeTypes.IDENTIFIER:
symbol = self.get_qual_name(node)
if symbol is not None:
value = self.join_symbol(nodetext, symbol)
case NodeTypes.INTERPRETED_STRING_LITERAL:
# TODO: don't use ast
value = ast.literal_eval(nodetext)
case NodeTypes.INT_LITERAL:
# TODO: hex, octal, binary
try:
value = int(nodetext)
except ValueError:
value = nodetext
case NodeTypes.FLOAT_LITERAL:
try:
value = float(nodetext)
except ValueError:
value = nodetext
case NodeTypes.TRUE:
value = True
case NodeTypes.FALSE:
value = False
case NodeTypes.NIL:
value = None
if node.type == NodeTypes.SELECTOR_EXPRESSION:
nodetext = node.string
symbol = self.get_qual_name(node)
if symbol is not None:
value = self.join_symbol(nodetext, symbol)
elif node.type == NodeTypes.IDENTIFIER:
symbol = self.get_qual_name(node)
if symbol is not None:
value = self.join_symbol(nodetext, symbol)
elif node.type == NodeTypes.INTERPRETED_STRING_LITERAL:
# TODO: don't use ast
value = ast.literal_eval(nodetext)
elif node.type == NodeTypes.INT_LITERAL:
# TODO: hex, octal, binary
try:
value = int(nodetext)
except ValueError:
value = nodetext
elif node.type == NodeTypes.FLOAT_LITERAL:
try:
value = float(nodetext)
except ValueError:
value = nodetext
elif node.type == NodeTypes.TRUE:
value = True
elif node.type == NodeTypes.FALSE:
value = False
elif node.type == NodeTypes.NIL:
value = None
except ValueError:
value = None

Expand Down
Loading

0 comments on commit d4135e3

Please sign in to comment.