diff --git a/bin/frama-c-script b/bin/frama-c-script index 0179f39113f1bc5b953a841c2fc23a41f0d5fc72..1843c1da6696c9873e12e26bdfb7b0153ab9eaf4 100755 --- a/bin/frama-c-script +++ b/bin/frama-c-script @@ -70,6 +70,22 @@ usage() { echo " and non-POSIX external libraries." echo " (run 'frama-c -machdep help' to get the list of machdeps)." echo "" + echo " - heuristic-print-callgraph [--dot outfile] file..." + echo " Prints a heuristic, syntactic-based, callgraph for the" + echo " specified files. Use --dot outfile to print it in DOT" + echo " (Graphviz) format, to [outfile]. If [outfile] is '-'," + echo " prints to stdout." + echo "" + echo " - heuristic-detect-recursion file..." + echo " Uses a heuristic, syntactic-based, callgraph to detect recursive" + echo " calls. Results are guaranteed neither correct nor complete." + echo "" + echo " - heuristic-list-functions want_defs want_decls file..." + echo " Uses heuristics to find possible function definitions and declarations." + echo " If [want_defs] is true, lists definitions." + echo " If [want_decls] is true, lists declarations." + echo " Results are guaranteed neither correct nor complete." + echo "" echo " - make-wrapper target arg..." echo " Runs 'make target arg...', parsing the output to suggest" echo " useful commands in case of failure." @@ -215,6 +231,18 @@ case "$command" in shift; configure_for_frama_c "$@"; ;; + "heuristic-print-callgraph") + shift; + ${FRAMAC_SHARE}/analysis-scripts/print_callgraph.py "$@"; + ;; + "heuristic-detect-recursion") + shift; + ${FRAMAC_SHARE}/analysis-scripts/detect_recursion.py "$@"; + ;; + "heuristic-list-functions") + shift; + ${FRAMAC_SHARE}/analysis-scripts/heuristic_list_functions.py "$@"; + ;; "make-wrapper") shift; "${FRAMAC_SHARE}"/analysis-scripts/make_wrapper.py "$0" "$@"; diff --git a/share/analysis-scripts/build_callgraph.py b/share/analysis-scripts/build_callgraph.py index 7d0e7e28c53c30721ff883ef99aaef1f727fc063..47a18b40477be12aaada19b262f5b2cd152e11bf 100755 --- a/share/analysis-scripts/build_callgraph.py +++ b/share/analysis-scripts/build_callgraph.py @@ -28,93 +28,126 @@ import sys import os import re -import glob import function_finder -MIN_PYTHON = (3, 5) # for glob(recursive) +MIN_PYTHON = (3, 5) if sys.version_info < MIN_PYTHON: sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) -debug = os.getenv("DEBUG") - arg = "" if len(sys.argv) < 2: - print(f"usage: {sys.argv[0]} [file1 file2 ...]") - print(" builds a heuristic callgraph for the specified files.") - sys.exit(1) + print(f"usage: {sys.argv[0]} file...") + print(" builds a heuristic callgraph for the specified files.") + sys.exit(1) else: - files = sys.argv[1:] - - -''' -re_fun = function_finder.prepare_definition_regex() -for f in files: - (found, match) = function_finder.find_first_match(re_fun, f) - if match: - fname = match.group(1) - else: - print(f"No function declaration or definition found in {f} !") - if found: - if found == 1: - print(f"Found declarator for {fname.upper()}, ignoring !") + files = sys.argv[1:] + +class Callgraph: + """ + Heuristics-based callgraphs. + Nodes are function names. Edges (caller, callee, locations) contain the source + and target nodes, plus a list of locations (file, line) where calls from + [caller] to [callee] occur. + """ + + # maps each caller to the list of its callees + succs = {} + + # maps (caller, callee) to the list of call locations + edges = {} + + def add_edge(self, caller, callee, loc): + if (caller, callee) in self.edges: + # edge already exists + self.edges[(caller, callee)].append(loc) else: - print(f"Found definition for {fname.upper()} !") -''' - -re_function_def = function_finder.prepare_definition_regex() -re_function_call = r"[a-zA-Z_][a-zA-Z0-9_]*\s*\(" -# here, get for each loop iteration a tuple (Match Object, int) of the name of the Match Object in file and it's line -# find_definitions is a generator function so appending all iterations results is to be expected in order to get a data_structure to further process -def function_definition_mapper(regex, File): - return [x for x in function_finder.find_definitions(regex, File, 0)] - -def function_calls_mapper(regex, File): - return [x for x in function_finder.find_calls(regex, File, 0)] - -# here starts the inspection of each of the files passed in command line -for f in files: - if debug: - print(f"Entering file {os.path.relpath(f)}:") - function_defs = function_definition_mapper(re_function_def, f) - if not function_defs: - if debug: - print(f"No call or potential call found in file {f} !") - continue - # function_ranges is a list of [name_function_def, (int, int)] - # its size == len(function_defs) - # name_function_def is a string - # (int, int) is a tuple of that describe the range of name_function_def - function_ranges = [] - [[function_ranges.append((element[0].group(1), (element[1], function_defs[i + 1][1] - 1)))\ - for i, element in enumerate(function_defs) if i < len(function_defs) - 1]] - function_calls = function_calls_mapper(re_function_call,f) - if not function_calls: - print(f"No definition found in file {f} !") - continue - if debug: - for i in range(len(function_ranges)): - print(f"{function_ranges[i]}") - print("\n") - for i in range(len(function_calls)): - print(f"{function_calls[i][0].group(0)} appears at line {function_calls[i][1]}") - print("\n") - func_def_calls = [] - # for each function call: - # Go through function_def list - # keep range of current function_def - # check if current function call is in that range - # append a tuple (string, string, int) as (function_being_defined, function_being_called, line of function call) to the list - for i in range(len(function_calls)): - for index, e in enumerate(function_ranges): - min_range, max_range = e[1] - if min_range < function_calls[i][1] <= max_range: - func_def_calls.append((e[0], function_calls[i][0].group(0)[:-1], function_calls[i][1])) - for i in range(len(func_def_calls)): - print(f"{os.path.relpath(f)}:{func_def_calls[i][2]}: {func_def_calls[i][0]} -> {func_def_calls[i][1]}") - - - - - - - + # new edge: check if caller exists + if not caller in self.succs: + self.succs[caller] = [] + # add callee as successor of caller + self.succs[caller].append(callee) + # add call location to edge information + self.edges[(caller, callee)] = [loc] + + def nodes(self): + return self.succs.keys() + + def __repr__(self): + return f"Callgraph({self.succs}, {self.edges})" + +def compute(files): + #print(f"Computing callgraph for {len(files)} file(s)...") + cg = Callgraph() + for f in files: + #print(f"Processing {os.path.relpath(f)}...") + newlines = function_finder.compute_newline_offsets(f) + defs = function_finder.find_definitions_and_declarations(True, False, f, newlines) + calls = function_finder.find_calls(f, newlines) + for call in calls: + caller = function_finder.find_caller(defs, call) + if caller: + called = call[0] + line = call[1] + loc = (f, line) + cg.add_edge(caller, called, loc) + #print(f"Callgraph computed ({len(cg.succs)} node(s), {len(cg.edges)} edge(s))") + return cg + +def print_edge(cg, caller, called, padding="", end="\n"): + locs = cg.edges[(caller, called)] + for (filename, line) in locs: + print(f"{padding}{os.path.relpath(filename)}:{line}: {caller} -> {called}", end=end) + +def print_cg(cg): + for (caller, called) in cg.edges: + print_edge(cg, caller, called) + +# succs: dict, input, not modified +# visited: set, input-output, modified +# just_visited: set, input-output, modified +# n: input, not modified +# +# The difference between visited and just_visited is that the latter refers +# to the current dfs; nodes visited in previous dfs already had their cycles +# reported, so we do not report them multiple times. +def cycle_dfs(cg, visited, just_visited, n): + just_visited.add(n) + if n not in cg.succs: + return [] + for succ in cg.succs[n]: + if succ in just_visited: + return [(n, succ)] + elif succ in visited: + # already reported in a previous iteration + return [] + else: + res = cycle_dfs(cg, visited, just_visited, succ) + if res: + caller = res[0][0] + return [(n, caller)] + res + else: + return [] + return [] + +def detect_recursion(cg): + #print(f"Detecting recursive calls...") + to_visit = set(cg.nodes()) + #if len(to_visit) > 100: + # print(f"Checking recursion ({len(to_visit)} nodes)...") + if not to_visit: # empty graph -> no recursion + return False + visited = set() + has_cycle = False + while to_visit: + just_visited = set() + n = sorted(list(to_visit))[0] + cycle = cycle_dfs(cg, visited, just_visited, n) + visited = visited.union(just_visited) + if cycle: + has_cycle = True + print(f"recursive cycle detected: ") + for (caller, called) in cycle: + print_edge(cg, caller, called, padding=" ") + to_visit -= visited + if not has_cycle: + print(f"no recursive calls detected") diff --git a/share/analysis-scripts/detect_recursion.py b/share/analysis-scripts/detect_recursion.py new file mode 100755 index 0000000000000000000000000000000000000000..ab3302352e8c03918212ded663b59add4c4c44e4 --- /dev/null +++ b/share/analysis-scripts/detect_recursion.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2020 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script finds files containing likely declarations and definitions +# for a given function name, via heuristic syntactic matching. + +import sys +import build_callgraph + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +arg = "" +if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} [file1 file2 ...]") + print(" prints a heuristic callgraph for the specified files.") + sys.exit(1) +else: + files = sys.argv[1:] + +cg = build_callgraph.compute(files) +build_callgraph.detect_recursion(cg) diff --git a/share/analysis-scripts/find_fun.py b/share/analysis-scripts/find_fun.py index 18588b293bd0452317f4ae72f90e8b396775c3a1..e50d3d3f287e7caa95998cc614ee3a82e8e9ff5b 100755 --- a/share/analysis-scripts/find_fun.py +++ b/share/analysis-scripts/find_fun.py @@ -73,9 +73,9 @@ print("Looking for '%s' inside %d file(s)..." % (fname, len(files))) possible_declarators = [] possible_definers = [] -re_fun = function_finder.prepare(fname) +re_fun = function_finder.prepare_re_specific_name(fname) for f in files: - found = function_finder.find(re_fun, f) + found = function_finder.find_specific_name(re_fun, f) if found: if found == 1: possible_declarators.append(f) @@ -95,7 +95,7 @@ else: relative_path = relative_path_to(reldir) if possible_declarators != []: print(f"Possible declarations for function '{fname}' in the following file(s){reldir_msg}:") - print(" " + "\n ".join([os.path.relpath(path, start=reldir) for path in possible_declarators])) + print(" " + "\n ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_declarators]))) if possible_definers != []: print(f"Possible definitions for function '{fname}' in the following file(s){reldir_msg}:") - print(" " + "\n ".join([os.path.relpath(path, start=reldir) for path in possible_definers])) + print(" " + "\n ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_definers]))) diff --git a/share/analysis-scripts/function_finder.py b/share/analysis-scripts/function_finder.py index 43a58ce665b413b8fc0e38467e63bbc747d60ad9..cfa761757e3e1bd34493e318a530eddcba4d620e 100755 --- a/share/analysis-scripts/function_finder.py +++ b/share/analysis-scripts/function_finder.py @@ -24,6 +24,8 @@ # Exports find_function_in_file, to be used by other scripts +import bisect +import os import re # To minimize the amount of false positives, we try to match the following: @@ -37,18 +39,27 @@ import re # - identifiers are allowed after the parentheses, to allow for some macros/ # modifiers +# auxiliary regexes +c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*" +c_id_maybe_pointer = c_identifier + "\**" +optional_c_id = "(?:" + c_identifier + ")?" +non_empty_whitespace = "[ \t\r\n]+" # includes newline/CR +whitespace = "[ \t\r\n]*" # includes newline/CR +type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*" + non_empty_whitespace + "\**" +optional_type_prefix = "(?:" + type_prefix + whitespace + ")?" +argument_list = "\([^)]*\)" + +debug = bool(os.getenv("DEBUG", False)) + # Precomputes the regex for 'fname' -def prepare(fname): - c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*" - c_id_maybe_pointer = c_identifier + "\**" - type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*\s+\**" - parentheses_suffix = "\s*\([^)]*\)" - re_fun = re.compile("^(?:" + type_prefix + "\s*)?" + fname + parentheses_suffix - + "\s*(?:" + c_identifier + ")?\s*(;|{)", flags=re.MULTILINE) +def prepare_re_specific_name(fname): + re_fun = re.compile("^" + optional_type_prefix + fname + whitespace + + argument_list + whitespace + + optional_c_id + whitespace + "(;|{)", flags=re.DOTALL | re.MULTILINE) return re_fun # Returns 0 if not found, 1 if declaration, 2 if definition -def find(prepared_re, f): +def find_specific_name(prepared_re, f): with open(f, encoding="ascii", errors='ignore') as content_file: content = content_file.read() has_decl_or_def = prepared_re.search(content) @@ -58,84 +69,138 @@ def find(prepared_re, f): is_decl = has_decl_or_def.group(1) == ";" return 1 if is_decl else 2 -# Precomputes the regex for a function -def prepare_definition_regex(): - c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*" - c_id_maybe_pointer = c_identifier + "\**" - type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*\s+\**" - parentheses_suffix = "\s*\([^)]*\)" - re_fun = re.compile("^(?:" + type_prefix + "\s*)?" + "([a-zA-Z_][a-zA-Z0-9_]*)" + parentheses_suffix - + "\s*(?:" + c_identifier + ")?\s*(;|{)", flags=re.MULTILINE) - return re_fun -# Returns tuple (0, None) if not found, (1, match) if declaration, (2, match) if definition -# Returns only at the first match of {prepared_re} regex in file {f}, not iterable unlike find3 function -def find_first_match(prepared_re, f): - with open(f, encoding="ascii", errors='ignore') as content_file: - content = content_file.read() - match = prepared_re.search(content) - if match is None: - return (0, None) - else: - is_decl = match.group(2) == ";" - return (1, match) if is_decl else (2, match) - - -def find_definitions(pattern, f, flags): - with open(f, encoding="ascii", errors='ignore') as content_file: - content = content_file.read() - # create a list of Match objects that fit "pattern" regex - matches = list(re.finditer(pattern, content, flags)) - if not matches: - return (None, 0) - # keep the offset of the last occurence of the matching pattern in the content string - # to avoiding checking past the last occurence in the newline_table - end = matches[-1].start() - newline_table = {-1: 0} - for i, m in enumerate(re.finditer(r'\n', content, 1)): - offset = m.start() - if offset > end: - break - # + 1 because convention states lines in a file start at 1 - # + 1 because we are counting newlines - newline_table[offset] = i + 1 + 1 - for m in matches: - newline_offset = content.rfind('\n', 0, m.start()) - # delete declarator and corresponding newline_table element if a ; is found instead of { - if m.group(2) == ';': - del(newline_table[newline_offset]) - del(m) +# matches function definitions +re_fundef_or_decl = re.compile("^" + optional_type_prefix + + "(" + c_identifier + ")" + whitespace + + argument_list + whitespace + + optional_c_id + whitespace + "(;|{)", + flags=re.DOTALL | re.MULTILINE) + +# matches function calls +re_funcall = re.compile("(" + c_identifier + ")" + whitespace + "\(") + +# Computes the offset (in bytes) of each '\n' in the file, +# returning them as a list +def compute_newline_offsets(filename): + offsets = [] + current = 0 + with open(filename, encoding="ascii", errors='ignore') as data: + for line in data: + current += len(line) + offsets.append(current) + return offsets + +# Returns the line number (starting at 1) containing the character +# of offset [offset]. +# [offsets] is the sorted list of offsets for newline characters in the file. +def line_of_offset(offsets, offset): + i = bisect.bisect_right(offsets, offset) + return i+1 + +# Returns the line number (starting at 1) of each line starting with '}' +# as its first character. +# +# This is a heuristic to attempt to detect function closing braces: +# it assumes that the first '}' (without preceding whitespace) after a +# function definition denotes its closing brace. +def compute_closing_braces(filename): + braces = [] + with open(filename, encoding="ascii", errors='ignore') as data: + for i, line in enumerate(data, start=1): + if line.startswith("}"): + braces.append(i) + return braces + +# Returns the first element of [line_numbers] greater than [n], or [None] +# if all numbers are smaller than [n] (this may happen e.g. when no +# closing braces were found). +# +# [line_numbers] must be sorted in ascending order. +def get_first_line_after(line_numbers, n): + for line in line_numbers: + if line > n: + return line + return None + +# Returns a list of tuples (fname, is_def, line_start, line_end, terminator_offset) +# for each function definition or declaration. +# If [want_defs] is True, definitions are included. +# If [want_decls] is True, declarations are included. +# [terminator_offset] is the byte offset of the `{` or `;`. +# The list is sorted w.r.t. line numbers (in ascending order). +# +# [terminator_offset] is used by the caller to filter the function prototype +# itself and avoid considering it as a call. For function definitions, +# this is the opening brace; for function declarations, this is the semicolon. +def find_definitions_and_declarations(want_defs, want_decls, filename, newlines): + braces = compute_closing_braces(filename) + with open(filename, encoding="ascii", errors='ignore') as data: + content = data.read() + res = [] + for match in re.finditer(re_fundef_or_decl, content): + funcname = match.group(1) + is_def = match.group(2) == "{" + is_decl = match.group(2) == ";" + assert is_def or is_decl + start = line_of_offset(newlines, match.start(1)) + if is_decl: + if not want_decls: + continue + end = line_of_offset(newlines, match.start(2)) else: - line_number = newline_table[newline_offset] - yield (m, line_number) - return (None, 0) - -def find_calls(pattern, f, flags): - with open(f, encoding="ascii", errors='ignore') as content_file: - content = content_file.read() - # create a list of Match objects that fit "pattern" regex - matches = list(re.finditer(pattern, content, flags)) - if not matches: - return (None, 0) - # Here, we filter all occurences of C keywords captured as possible func calls - # replace `filtre` assignement by a more exhaustive list for better results - filtre = ["if", "return", "while", "for"] - matches = [n for n in matches if not any(m in n[0] for m in filtre)] - - # keep the offset of the last occurence of the matching pattern in the content string - # to avoiding checking past the last occurence in the newline_table - end = matches[-1].start() - newline_table = {-1: 0} - for i, m in enumerate(re.finditer(r'\n', content, 1)): - offset = m.start() - if offset > end: - break - # + 1 because convention states lines in a file start at 1 - # + 1 because we are counting newlines - newline_table[offset] = i + 1 + 1 - - for m in matches: - newline_offset = content.rfind('\n', 0, m.start()) - line_number = newline_table[newline_offset] - yield (m, line_number) - return (None, 0) + if not want_defs: + continue + end = get_first_line_after(braces, start) + if not end: + # no closing braces found; use "single-line function heuristic": + # assume the function is defined as 'type f(...) { code; }', + # in a single line + def_start_newline_offset = newlines[start-1] + line_of_opening_brace = line_of_offset(newlines, match.start(2)) + definition = content[match.start(1):newlines[start-1]] + if start == line_of_opening_brace and definition.rstrip().endswith("}"): + # assume the '}' is closing the '{' from the same line + end = line_of_opening_brace + else: + # no opening brace; assume a false positive and skip definition + print(f"{os.path.relpath(filename)}:{start}:closing brace not found, " + + f"skipping potential definition of '{funcname}'") + continue + terminator_offset = match.start(2) + res.append((funcname, is_def, start, end, terminator_offset)) + return res + +# list of identifiers which are never function calls +calls_blacklist = ["if", "while", "for", "return"] + +# Returns a list of tuples (fname, line, offset) for each function call. +# +# Note: this may include the function prototype itself; +# it must be filtered by the caller. +def find_calls(filename, newlines): + with open(filename, encoding="ascii", errors='ignore') as data: + content = data.read() + # create a list of Match objects that fit "pattern" regex + res = [] + for match in re.finditer(re_funcall, content): + funcname = match.group(1) + offset = match.start(1) + line = line_of_offset(newlines, offset) + if funcname not in calls_blacklist: + res.append((funcname, line, offset)) + return res + +# Returns the caller of [call], that is, the function whose definition +# contains the line where [call] happens. +# Returns [None] if there is no function at such line (i.e. a false positive). +# +# [defs] must be sorted in ascending order. +def find_caller(defs, call): + (called, line, offset) = call + for (fname, _is_def, start, end, brace_offset) in defs: + if line >= start and line <= end and offset > brace_offset: + return fname + elif start > line: + return None + return None diff --git a/share/analysis-scripts/heuristic_list_functions.py b/share/analysis-scripts/heuristic_list_functions.py new file mode 100755 index 0000000000000000000000000000000000000000..86df5e9d7fcc53324371749b4dcaf65d4e10c4c6 --- /dev/null +++ b/share/analysis-scripts/heuristic_list_functions.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2020 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script uses heuristics to list all function definitions and +# declarations in a set of files. + +import sys +import os +import re +import function_finder + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +debug = bool(os.getenv("DEBUG", False)) + +arg = "" +if len(sys.argv) < 4: + print("usage: %s want_defs want_decls file..." % sys.argv[0]) + print(" looks for likely function definitions and/or declarations") + print(" in the specified files.") + sys.exit(1) + +want_defs = sys.argv[1] +want_decls = sys.argv[2] +files = sys.argv[3:] + +for f in files: + newlines = function_finder.compute_newline_offsets(f) + defs_and_decls = function_finder.find_definitions_and_declarations(want_defs, want_decls, f, newlines) + for (funcname, is_def, start, end, _offset) in defs_and_decls: + if is_def: + print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (definition)") + else: + print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (declaration)") diff --git a/share/analysis-scripts/make_template.py b/share/analysis-scripts/make_template.py index c986e4551c446b47caff148e869d65ef1713c58d..6e47b60c60993be74bbc2d40a32e9cd45150f880 100755 --- a/share/analysis-scripts/make_template.py +++ b/share/analysis-scripts/make_template.py @@ -98,11 +98,11 @@ main = input("Main target name: ") if not re.match("^[a-zA-Z_0-9-]+$", main): sys.exit("error: invalid main target name (can only contain letters, digits, dash or underscore)") -main_fun_finder_re = function_finder.prepare("main") +main_fun_finder_re = function_finder.prepare_re_specific_name("main") # returns 0 if none, 1 if declaration, 2 if definition def defines_or_declares_main(f): - return function_finder.find(main_fun_finder_re, f) + return function_finder.find_specific_name(main_fun_finder_re, f) def expand_and_normalize_sources(expression, relprefix): subexps = shlex.split(expression) diff --git a/share/analysis-scripts/print_callgraph.py b/share/analysis-scripts/print_callgraph.py new file mode 100755 index 0000000000000000000000000000000000000000..5071c4ba4ccfe85c3066eb77048169326aaec3ab --- /dev/null +++ b/share/analysis-scripts/print_callgraph.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2020 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script finds files containing likely declarations and definitions +# for a given function name, via heuristic syntactic matching. + +import sys +import build_callgraph + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +arg = "" +if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} [file1 file2 ...]") + print(" prints a heuristic callgraph for the specified files.") + sys.exit(1) +else: + files = sys.argv[1:] + +cg = build_callgraph.compute(files) +build_callgraph.print_cg(cg) diff --git a/tests/fc_script/build-callgraph.i b/tests/fc_script/build-callgraph.i new file mode 100644 index 0000000000000000000000000000000000000000..b8105502931616ce2861af2459303855112b449a --- /dev/null +++ b/tests/fc_script/build-callgraph.i @@ -0,0 +1,69 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG build-callgraph.res LOG build-callgraph.err bin/frama-c-script heuristic-print-callgraph @PTEST_DIR@/@PTEST_NAME@.i > @PTEST_DIR@/result/build-callgraph.res 2> @PTEST_DIR@/result/build-callgraph.err + */ + +#include <stdio.h> +void main() { + strlen(""); +} +struct s { + int a; int b; +} s; + +volatile int v; + +int fn2(int, int); + +int fn1(int x, int y) +{ + Frama_C_show_each_1(x); + Frama_C_show_each_2(y); + return x + y; +} + +int X, Y; +int main1 () { + R1 = fn1(G, G|0); + R2 = fn2(G, G|0); + Frama_C_show_each_d(G); + pv = (int *) &X; + return Y; +} + +int * main2() { + return 0; +} + +#define not_a_function_call(v) \ + yes_a_function_call(v) + +#define yet_another_not_a_call(v) do { \ + yes_again(); \ + } while (0) + +/* call_inside_comment(evaluation); */ +void main3 () { + //@ not_a_function_call(v); + yet_another_not_a_call(v); +} + +/* Tests the initialization of local variables. */ +void main4 () { + f(g(h(i, j), k (l, m ( n( + o, p( + q) + ) + ) + ) + ) + ); + f(); + g(); +} +void main() { + main1(); + main2(); + main3(); + main4(); +} diff --git a/tests/fc_script/list_functions.i b/tests/fc_script/list_functions.i new file mode 100644 index 0000000000000000000000000000000000000000..f30547abb73355730c35f523382b6b37a21f7aea --- /dev/null +++ b/tests/fc_script/list_functions.i @@ -0,0 +1,4 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG list_functions.res LOG list_functions.err bin/frama-c-script heuristic-list-functions @PTEST_DIR@/*.c @PTEST_DIR@/*.i > @PTEST_DIR@/result/list_functions.res 2> @PTEST_DIR@/result/list_functions.err + */ diff --git a/tests/fc_script/oracle/build-callgraph.err b/tests/fc_script/oracle/build-callgraph.err new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/fc_script/oracle/build-callgraph.res b/tests/fc_script/oracle/build-callgraph.res new file mode 100644 index 0000000000000000000000000000000000000000..145f1bccaf869fe75bcfed8dbd2834e13396ee33 --- /dev/null +++ b/tests/fc_script/oracle/build-callgraph.res @@ -0,0 +1,21 @@ +tests/fc_script/build-callgraph.i:8: main -> strlen +tests/fc_script/build-callgraph.i:20: fn1 -> Frama_C_show_each_1 +tests/fc_script/build-callgraph.i:21: fn1 -> Frama_C_show_each_2 +tests/fc_script/build-callgraph.i:27: main1 -> fn1 +tests/fc_script/build-callgraph.i:28: main1 -> fn2 +tests/fc_script/build-callgraph.i:29: main1 -> Frama_C_show_each_d +tests/fc_script/build-callgraph.i:47: main3 -> not_a_function_call +tests/fc_script/build-callgraph.i:48: main3 -> yet_another_not_a_call +tests/fc_script/build-callgraph.i:53: main4 -> f +tests/fc_script/build-callgraph.i:61: main4 -> f +tests/fc_script/build-callgraph.i:53: main4 -> g +tests/fc_script/build-callgraph.i:62: main4 -> g +tests/fc_script/build-callgraph.i:53: main4 -> h +tests/fc_script/build-callgraph.i:53: main4 -> k +tests/fc_script/build-callgraph.i:53: main4 -> m +tests/fc_script/build-callgraph.i:53: main4 -> n +tests/fc_script/build-callgraph.i:54: main4 -> p +tests/fc_script/build-callgraph.i:65: main -> main1 +tests/fc_script/build-callgraph.i:66: main -> main2 +tests/fc_script/build-callgraph.i:67: main -> main3 +tests/fc_script/build-callgraph.i:68: main -> main4 diff --git a/tests/fc_script/oracle/find_fun1.res b/tests/fc_script/oracle/find_fun1.res index 0ceb3c8c68b411868c7fd156287a3c668e09466c..7d423014f36cda2c6e9e7f405bb272dd0f42d8e3 100644 --- a/tests/fc_script/oracle/find_fun1.res +++ b/tests/fc_script/oracle/find_fun1.res @@ -1,5 +1,6 @@ -Looking for 'main2' inside 11 file(s)... +Looking for 'main2' inside 14 file(s)... Possible declarations for function 'main2' in the following file(s): tests/fc_script/for-find-fun.c Possible definitions for function 'main2' in the following file(s): + tests/fc_script/build-callgraph.i tests/fc_script/main2.c diff --git a/tests/fc_script/oracle/find_fun2.res b/tests/fc_script/oracle/find_fun2.res index e59924a72f49f8278f2ff8fcec02062b9c68b2e3..ded016c9272e4dccb55c80cbb45e4fe3115c214b 100644 --- a/tests/fc_script/oracle/find_fun2.res +++ b/tests/fc_script/oracle/find_fun2.res @@ -1,5 +1,6 @@ -Looking for 'main3' inside 11 file(s)... +Looking for 'main3' inside 14 file(s)... Possible declarations for function 'main3' in the following file(s): tests/fc_script/for-find-fun2.c Possible definitions for function 'main3' in the following file(s): + tests/fc_script/build-callgraph.i tests/fc_script/for-find-fun.c diff --git a/tests/fc_script/oracle/find_fun3.res b/tests/fc_script/oracle/find_fun3.res index 6d151d463efe8d8df1a1ac874653880564ee4eb1..4a42d9117089960eb086c47a8e4b07b44795e3c3 100644 --- a/tests/fc_script/oracle/find_fun3.res +++ b/tests/fc_script/oracle/find_fun3.res @@ -1,2 +1,2 @@ -Looking for 'false_positive' inside 11 file(s)... +Looking for 'false_positive' inside 14 file(s)... No declaration/definition found for function 'false_positive' diff --git a/tests/fc_script/oracle/recursions.err b/tests/fc_script/oracle/recursions.err new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/fc_script/oracle/recursions.res b/tests/fc_script/oracle/recursions.res new file mode 100644 index 0000000000000000000000000000000000000000..784a72aeb5ffd1c48837607bd3707eff14280562 --- /dev/null +++ b/tests/fc_script/oracle/recursions.res @@ -0,0 +1,8 @@ +recursive cycle detected: + tests/fc_script/recursions.i:13: f -> f +recursive cycle detected: + tests/fc_script/recursions.i:18: h -> h +recursive cycle detected: + tests/fc_script/recursions.i:34: k -> l + tests/fc_script/recursions.i:38: l -> m + tests/fc_script/recursions.i:42: m -> k diff --git a/tests/fc_script/recursions.i b/tests/fc_script/recursions.i new file mode 100644 index 0000000000000000000000000000000000000000..d471e8c461858cf371f040e13d2abb1e1763b0fa --- /dev/null +++ b/tests/fc_script/recursions.i @@ -0,0 +1,46 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG recursions.res LOG recursions.err bin/frama-c-script heuristic-detect-recursion @PTEST_FILE@ > @PTEST_DIR@/result/recursions.res 2> @PTEST_DIR@/result/recursions.err +*/ + +volatile int v; + +void g() { + int g = 42; +} + +void f() { + if (v) f(); + else g(); +} + +void h() { + if (v) h(); + else g(); +} + +void i() { + g(); +} + +void j() { + f(); +} + +void l(void); +void m(void); + +void k() { + if (v) l(); +} + +void l() { + if (v) m(); +} + +void m() { + if (v) k(); +} + +void norec() { +}