Skip to content

Commit 83df9c6

Browse files
committed
add more checkers
1 parent 820506e commit 83df9c6

20 files changed

+1154
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
This specification ensures that the elements of an array are comparable before sorting them.
10+
Source: https://docs.python.org/3/library/functions.html#sorted.
11+
"""
12+
13+
14+
class Arrays_Comparable(BaseDyLinAnalysis):
15+
16+
def __init__(self, **kwargs) -> None:
17+
super().__init__(**kwargs)
18+
self.analysis_name = "Arrays_Comparable"
19+
20+
@only(patterns=["sorted"])
21+
def pre_call(
22+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
23+
) -> None:
24+
# The target class names for monitoring
25+
targets = ["builtins"]
26+
27+
# Get the class name
28+
if hasattr(function, '__module__'):
29+
class_name = function.__module__
30+
else:
31+
class_name = None
32+
33+
# Check if the class name is the target ones
34+
if class_name in targets:
35+
36+
# Spec content
37+
objs = pos_args[0]
38+
if isinstance(objs, list):
39+
new_objs = objs[:] # Shallow copy the elements in the list inputted.
40+
if kw_args.get('key'): # If a key method for comparison is provided.
41+
key = kw_args['key'] # Store the key method.
42+
for i in range(len(new_objs)): # Convert the elements using the inputted key method.
43+
new_objs[i] = key(new_objs[i])
44+
try: # Check if the object is comparable.
45+
for i in range(len(new_objs)):
46+
for j in range(i + 1, len(new_objs)):
47+
# This will raise a TypeError if elements at i and j are not comparable.
48+
_ = new_objs[i] < new_objs[j]
49+
except TypeError:
50+
self.add_finding(
51+
iid,
52+
dyn_ast,
53+
"B-1",
54+
f"Array with non-comparable elements is about to be sorted at {dyn_ast}."
55+
)
56+
# =========================================================================
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
This specification warns if close() is invoked on sys.stderr which is a useless invocation.
10+
Source: https://docs.python.org/3/faq/library.html#why-doesn-t-closing-sys-stdout-stdin-stderr-really-close-it.
11+
"""
12+
13+
14+
class Console_CloseErrorWriter(BaseDyLinAnalysis):
15+
16+
def __init__(self, **kwargs) -> None:
17+
super().__init__(**kwargs)
18+
self.analysis_name = "Console_CloseErrorWriter"
19+
20+
@only(patterns=["close"])
21+
def pre_call(
22+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
23+
) -> None:
24+
# The target class names for monitoring
25+
targets = ["<stderr>"]
26+
27+
# Get the class name
28+
if hasattr(function, '__self__') and hasattr(function.__self__, 'name'):
29+
class_name = function.__self__.name
30+
else:
31+
class_name = None
32+
33+
# Check if the class name is the target ones
34+
if class_name in targets:
35+
36+
# Spec content
37+
self.add_finding(
38+
iid,
39+
dyn_ast,
40+
"B-2",
41+
f"close() is invoked on sys.stderr which is a useless invocation at {dyn_ast}."
42+
)
43+
# =========================================================================
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
This specification warns if close() is invoked on sys.stdin which is a useless invocation.
10+
Source: https://docs.python.org/3/faq/library.html#why-doesn-t-closing-sys-stdout-stdin-stderr-really-close-it.
11+
"""
12+
13+
14+
class Console_CloseReader(BaseDyLinAnalysis):
15+
16+
def __init__(self, **kwargs) -> None:
17+
super().__init__(**kwargs)
18+
self.analysis_name = "Console_CloseReader"
19+
20+
@only(patterns=["close"])
21+
def pre_call(
22+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
23+
) -> None:
24+
# The target class names for monitoring
25+
targets = ["<stdin>"]
26+
27+
# Get the class name
28+
if hasattr(function, '__self__') and hasattr(function.__self__, 'name'):
29+
class_name = function.__self__.name
30+
else:
31+
class_name = None
32+
33+
# Check if the class name is the target ones
34+
if class_name in targets:
35+
36+
# Spec content
37+
self.add_finding(
38+
iid,
39+
dyn_ast,
40+
"B-3",
41+
f"close() is invoked on sys.stdin which is a useless invocation at {dyn_ast}."
42+
)
43+
# =========================================================================
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
This specification warns if close() is invoked on sys.stdout which is a useless invocation.
10+
Source: https://docs.python.org/3/faq/library.html#why-doesn-t-closing-sys-stdout-stdin-stderr-really-close-it.
11+
"""
12+
13+
14+
class Console_CloseWriter(BaseDyLinAnalysis):
15+
16+
def __init__(self, **kwargs) -> None:
17+
super().__init__(**kwargs)
18+
self.analysis_name = "Console_CloseWriter"
19+
20+
@only(patterns=["close"])
21+
def pre_call(
22+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
23+
) -> None:
24+
# The target class names for monitoring
25+
targets = ["<stdout>"]
26+
27+
# Get the class name
28+
if hasattr(function, '__self__') and hasattr(function.__self__, 'name'):
29+
class_name = function.__self__.name
30+
else:
31+
class_name = None
32+
33+
# Check if the class name is the target ones
34+
if class_name in targets:
35+
36+
# Spec content
37+
self.add_finding(
38+
iid,
39+
dyn_ast,
40+
"B-4",
41+
f"close() is invoked on sys.stdout which is a useless invocation at {dyn_ast}."
42+
)
43+
# =========================================================================
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
This specification ensures that canvas widgets are added only to the CanvasFrame's designated canvas
10+
source: https://www.nltk.org/api/nltk.draw.util.html#nltk.draw.util.CanvasFrame.add_widget
11+
"""
12+
13+
14+
class CreateWidgetOnSameFrameCanvas(BaseDyLinAnalysis):
15+
16+
def __init__(self, **kwargs) -> None:
17+
super().__init__(**kwargs)
18+
self.analysis_name = "CreateWidgetOnSameFrameCanvas"
19+
20+
@only(patterns=["add_widget"])
21+
def pre_call(
22+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
23+
) -> None:
24+
# The target class names for monitoring
25+
targets = ["nltk.draw.util.CanvasFrame"]
26+
27+
# Get the class name
28+
if hasattr(function, '__self__') and hasattr(function.__self__, '__class__'):
29+
cls = function.__self__.__class__
30+
class_name = cls.__module__ + "." + cls.__name__
31+
else:
32+
class_name = None
33+
34+
# Check if the class name is the target ones
35+
if class_name in targets:
36+
37+
# Spec content
38+
args = pos_args
39+
kwargs = kw_args
40+
41+
canvasFrame = function.__self__ # Updated to use the self object
42+
canvasWidget = None
43+
44+
if len(args) > 1:
45+
canvasWidget = args[1]
46+
else:
47+
canvasWidget = kwargs['canvaswidget']
48+
49+
fCanvas = canvasFrame.canvas()
50+
wCanvas = canvasWidget.canvas()
51+
52+
# TODO: Do we need to recursively check the children of the CanvasWidget?
53+
# Logically, it makes sense, but docs don't mention it directly.
54+
55+
if wCanvas.winfo_id() != fCanvas.winfo_id():
56+
57+
# Spec content
58+
self.add_finding(
59+
iid,
60+
dyn_ast,
61+
"B-5",
62+
f"CanvasWidget must be created on the same canvas as the CanvasFrame it is being added to at {dyn_ast}."
63+
)
64+
# =========================================================================
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
7+
8+
"""
9+
It is recommended to terminate full hostnames with a /.
10+
"""
11+
12+
13+
class HostnamesTerminatesWithSlash(BaseDyLinAnalysis):
14+
15+
def __init__(self, **kwargs) -> None:
16+
super().__init__(**kwargs)
17+
self.analysis_name = "HostnamesTerminatesWithSlash"
18+
19+
@only(patterns=["mount"])
20+
def pre_call(
21+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
22+
) -> None:
23+
# The target class names for monitoring
24+
targets = ["requests.sessions.Session"]
25+
26+
# Get the class name
27+
if hasattr(function, '__self__') and hasattr(function.__self__, '__class__'):
28+
cls = function.__self__.__class__
29+
class_name = cls.__module__ + "." + cls.__name__
30+
else:
31+
class_name = None
32+
33+
# Check if the class name is the target ones
34+
if class_name in targets:
35+
36+
# Spec content
37+
url = pos_args[0] # Updated to use the first argument as self is not considered here
38+
if not url.endswith('/'):
39+
40+
# Spec content
41+
self.add_finding(
42+
iid,
43+
dyn_ast,
44+
"B-6",
45+
f"The call to method mount in file at {dyn_ast} does not terminate the hostname with a /."
46+
)
47+
# =========================================================================
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ============================== Define spec ==============================
2+
from .base_analysis import BaseDyLinAnalysis
3+
from dynapyt.instrument.filters import only
4+
5+
from typing import Callable, Tuple, Dict
6+
import re
7+
8+
9+
"""
10+
RegexpTokenizer pattern must not contain capturing parentheses
11+
src: https://www.nltk.org/api/nltk.tokenize.regexp.html
12+
"""
13+
14+
15+
def contains_capturing_groups(pattern):
16+
regex = re.compile(pattern)
17+
18+
if regex.groups > 0:
19+
# Further check to distinguish capturing from non-capturing by examining the pattern
20+
# This involves checking all group occurrences in the pattern
21+
# We need to avoid matching escaped parentheses \( or \) and non-capturing groups (?: ...)
22+
non_capturing = re.finditer(r'\(\?[:=!]', pattern)
23+
non_capturing_indices = {match.start() for match in non_capturing}
24+
25+
# Finding all parentheses that could start a group
26+
all_groups = re.finditer(r'\((?!\?)', pattern)
27+
for match in all_groups:
28+
if match.start() not in non_capturing_indices:
29+
return True # Found at least one capturing group
30+
return False
31+
else:
32+
return False
33+
34+
35+
class NLTK_RegexpTokenizerCapturingParentheses(BaseDyLinAnalysis):
36+
37+
def __init__(self, **kwargs) -> None:
38+
super().__init__(**kwargs)
39+
self.analysis_name = "NLTK_RegexpTokenizerCapturingParentheses"
40+
41+
@only(patterns=["RegexpTokenizer"])
42+
def pre_call(
43+
self, dyn_ast: str, iid: int, function: Callable, pos_args: Tuple, kw_args: Dict
44+
) -> None:
45+
# The target class names for monitoring
46+
targets = ["nltk.tokenize.regexp.RegexpTokenizer"]
47+
48+
# Get the class name
49+
if hasattr(function, '__module__') and hasattr(function, '__name__'):
50+
class_name = function.__module__ + "." + function.__name__
51+
else:
52+
class_name = None
53+
54+
# Check if the class name is the target ones
55+
if class_name in targets:
56+
57+
# Spec content
58+
pattern = None
59+
if kw_args.get('pattern'):
60+
pattern = kw_args['pattern']
61+
elif len(pos_args) > 1:
62+
pattern = pos_args[1]
63+
64+
# Check if the regular expression is empty
65+
if pattern is not None and contains_capturing_groups(pattern):
66+
67+
# Spec content
68+
self.add_finding(
69+
iid,
70+
dyn_ast,
71+
"B-9",
72+
f"Must use non_capturing parentheses for RegexpTokenizer pattern at {dyn_ast}."
73+
)
74+
# =========================================================================

0 commit comments

Comments
 (0)