Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,140 @@ def _remove_c_comments(code: str) -> str:
code = re.sub(r'//.*', '', code)
return code


def _count_c_declared_params(func_doc: Document) -> int | None:
"""Count declared parameters in a C function definition.
Returns None if the function signature cannot be parsed (e.g. macros, variadic)."""
code = func_doc.page_content.strip().replace('\r\n', '\n')
m = _FUNCTION_PATTERN_REGEX.search(code, timeout=REGEX_TIMEOUT_SECONDS)
if not m:
return None
params = m.group('params').strip()
if not params or params == 'void':
return 0
if '...' in params:
return None
depth = 0
count = 1
for ch in params:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
elif ch == ',' and depth == 0:
count += 1
return count


def _extract_call_site_args_str(function_body: str, start_pos: int) -> str | None:
"""Extract the arguments string from a call site starting at the opening paren.
Returns None if the call site cannot be parsed.

Correctly handles string literals, char literals, and nested parentheses.
"""
depth = 0
end_pos = -1
in_string = False
in_char = False
i = start_pos
while i < len(function_body):
ch = function_body[i]
if in_string:
if ch == '\\' and i + 1 < len(function_body):
i += 2
continue
if ch == '"':
in_string = False
elif in_char:
if ch == '\\' and i + 1 < len(function_body):
i += 2
continue
if ch == "'":
in_char = False
else:
if ch == '"':
in_string = True
elif ch == "'":
in_char = True
elif ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
end_pos = i
break
i += 1
if end_pos == -1:
return None
return function_body[start_pos + 1:end_pos].strip()


def _count_args_in_str(args_str: str) -> int:
"""Count the number of arguments in an argument string.

Correctly handles commas inside string literals, char literals, and nested parentheses.
"""
if not args_str:
return 0
depth = 0
count = 1
in_string = False
in_char = False
i = 0
while i < len(args_str):
ch = args_str[i]
if in_string:
if ch == '\\' and i + 1 < len(args_str):
i += 2
continue
if ch == '"':
in_string = False
elif in_char:
if ch == '\\' and i + 1 < len(args_str):
i += 2
continue
if ch == "'":
in_char = False
else:
if ch == '"':
in_string = True
elif ch == "'":
in_char = True
elif ch == '(':
depth += 1
elif ch == ')':
depth -= 1
elif ch == ',' and depth == 0:
count += 1
i += 1
return count


def _count_all_call_site_args(function_body: str, func_name: str) -> list[int]:
"""Count arguments at ALL call sites of func_name(...) in function_body.
Returns a list of argument counts for each call site found.

Correctly handles commas inside string literals, char literals, and nested parentheses.
"""
pattern = re.compile(r'\b' + re.escape(func_name) + r'\s*\(')
results = []
for m in pattern.finditer(function_body):
start = m.end() - 1
args_str = _extract_call_site_args_str(function_body, start)
if args_str is not None:
results.append(_count_args_in_str(args_str))
return results


def _count_call_site_args(function_body: str, func_name: str) -> int | None:
"""Count arguments at the first call site of func_name(...) in function_body.
Returns None if no call site is found.

Correctly handles commas inside string literals, char literals, and nested parentheses.
"""
counts = _count_all_call_site_args(function_body, func_name)
return counts[0] if counts else None

# Compile the function pattern with recursive param matching
_FUNCTION_PATTERN_REGEX = regex.compile(r'''

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RedTanny Better relocate this regex pattern to the top of the module, so it will be before the function that uses that, for a better readability of the logic and code.

^[ \t]* # Leading indentation
Expand Down Expand Up @@ -533,15 +667,15 @@ def search_for_called_function(
self,
caller_function: Document,
callee_function_name: str,
callee_function_package: str, # For C, this is usually the file or library name
callee_function: Document,
callee_function_package: str,
Comment on lines +670 to +671

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RedTanny Until now it was different then the signature of the Parent ABC huh?
But probably it wasn't mattered too much, as C using BFS instead of DFS in generic algorithm of CCA

code_documents: list[Document],
type_documents: list[Document],
callee_function_file_name: str,
fields_of_types: dict[tuple, list[tuple]],
functions_local_variables_index: dict[str, dict],
documents_of_functions: list[Document] = None,
type_inheritance: dict[Tuple[str, str], List[Tuple[str, str]]] = None,
callee_function:Document = None
) -> bool:
"""
Returns True if caller_function calls callee_function (directly or via struct/function pointer).
Expand All @@ -551,7 +685,22 @@ def search_for_called_function(
# 2. Direct call: callee_function_name(
direct_call_pattern = re.compile(r'\b' + re.escape(callee_function_name) + r'\s*\(')
if direct_call_pattern.search(function_body):
return True
if callee_function is not None:
declared = _count_c_declared_params(callee_function)
if declared is None:
return True
all_call_args = _count_all_call_site_args(function_body, callee_function_name)
if not all_call_args:
return True
if declared in all_call_args:
return True
logger.debug(
"Argument count mismatch for %s: declared=%d, call_sites=%s in %s — rejecting false match",
callee_function_name, declared, all_call_args,
caller_function.metadata.get('source', '?'))
return False
else:
return True

# 3. Struct member or pointer call: obj->callee_function_name( or obj.callee_function_name(
member_call_pattern = re.compile(
Expand Down Expand Up @@ -750,9 +899,6 @@ def is_call_allowed(self, pkg_docs: list[Document], caller_function: Document, c

callee_name = self.get_function_name(callee_function)

if callee_name == "do_shell":
print(f"callee_name: {callee_name}")

if callee_name in caller_functions:
doc = caller_functions[callee_name]
# if static and in same file → call is allowed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def get_package_name_file(function: Document):
class GoLanguageFunctionsParser(LanguageFunctionsParser):

def is_same_package(self, package_name_from_input, package_name_from_tree):
if not package_name_from_input or not package_name_from_tree:
return False
return package_name_from_input.lower() in package_name_from_tree.lower()

def is_tree_key_match(self, package_from_doc: str, tree_key: str) -> bool:
Expand All @@ -66,6 +68,19 @@ def is_tree_key_match(self, package_from_doc: str, tree_key: str) -> bool:
return True
return False

def resolve_subpackage_to_module(self, subpackage: str, tree_key: str) -> str | None:
"""Return the sub-package suffix when *subpackage* is a child of *tree_key*.

Example: subpackage='google.golang.org/protobuf/encoding/protojson',
tree_key='google.golang.org/protobuf'
→ returns '/encoding/protojson'
"""
a = subpackage.lower()
b = tree_key.lower()
if a.startswith(b + "/"):
return subpackage[len(tree_key):]
return None

def get_dummy_function(self, function_name):
return f"{self.get_function_reserved_word()} {function_name}() {{}}"

Expand Down Expand Up @@ -289,7 +304,7 @@ def parse_all_type_struct_class_to_fields(self, types: list[Document], type_inhe
[name, _, type_name] = declaration_parts
elif len(declaration_parts) == 2:
[name, type_name] = declaration_parts
if len(declaration_parts) == (2 or 3):
if len(declaration_parts) in (2, 3):
self.parse_one_type(Document(page_content=f"type {name} {type_name}",
metadata={"source": the_type.metadata['source']}),
types_mapping)
Expand Down Expand Up @@ -431,7 +446,6 @@ def search_for_called_function(self, caller_function: Document, callee_function_
index_of_function_closing = caller_function.page_content.rfind("}")
caller_function_body = str(
caller_function.page_content[index_of_function_opening + 1: index_of_function_closing])
re.search("", caller_function_body)
escaped_name = re.escape(callee_function_name)
regex = fr'(?<![a-zA-Z0-9_])(?:[a-zA-Z0-9_\[\]\(\).]*\.)?{escaped_name}\('
matching = re.search(regex, caller_function_body, re.MULTILINE)
Expand Down Expand Up @@ -627,17 +641,26 @@ def is_package_imported(self, code_content: str, identifier: str, callee_package
start_of_package_name = code_content[identifier_import_position + len("import ")
+ len(identifier):]
index_of_end_of_line = start_of_package_name.find(os.linesep)
package_name_to_check = start_of_package_name[:index_of_end_of_line]
package_name_to_check = start_of_package_name[:index_of_end_of_line].strip().strip("'\"")
if package_name_to_check.strip().lower() == callee_package.strip().lower():
return True
# import without alias, in this case maybe package name contain alias
else:
# re.search(regex, caller_function_body, re.MULTILINE)
matching = re.search(rf"import [\'\"].*{identifier}[\'\"]", code_content)
esc_id = re.escape(identifier)
# NOTE: The trailing .* is intentional to match identifiers in:
# - Versioned module paths (e.g., 'jwt' in 'github.com/golang-jwt/jwt/v5')
# - Hyphenated package names (e.g., 'jose' in 'gopkg.in/go-jose/go-jose.v2')
# This may produce false positives (e.g., 'json' matching 'json-iterator/go'),
# but those only add extra candidates to verify, not incorrect final results.
matching = re.search(rf"import [\'\"].*{esc_id}.*[\'\"]", code_content)
if matching and matching.group(0):
import_line = code_content[matching.start():]
import_package_line = import_line[:import_line.find(os.linesep)].strip()
package_name = import_package_line.split(r"\s")[1]
parts = import_package_line.split()
if len(parts) < 2:
return False
package_name = parts[1].strip("'\"")
if package_name.strip().lower() == callee_package.strip().lower():
return True
return False
Expand Down
Loading