|
| 1 | +# This script is designed to run with Understand - CodeCheck |
| 2 | +# Written by Jason Quinn |
| 3 | +# 2026-05-07 |
| 4 | + |
| 5 | + |
| 6 | +ERR1 = 'Return value of "%1" is discarded; error information is not tested' |
| 7 | + |
| 8 | +# Common standard-library functions that return error information. Per the |
| 9 | +# Amplification, the list is project-specific; this default list covers the |
| 10 | +# most-frequently misused C standard-library functions. |
| 11 | +DEFAULT_ERROR_FUNCS = [ |
| 12 | + # Memory allocation |
| 13 | + 'malloc', 'calloc', 'realloc', 'aligned_alloc', |
| 14 | + # File handling |
| 15 | + 'fopen', 'freopen', 'tmpfile', |
| 16 | + 'fclose', 'fflush', |
| 17 | + 'fread', 'fwrite', |
| 18 | + 'fseek', 'ftell', 'fsetpos', 'fgetpos', |
| 19 | + 'fputc', 'fputs', 'putc', 'putchar', 'puts', |
| 20 | + 'fgetc', 'fgets', 'getc', 'getchar', |
| 21 | + 'ungetc', |
| 22 | + 'remove', 'rename', |
| 23 | + 'setvbuf', |
| 24 | + # Formatted I/O |
| 25 | + 'fprintf', 'printf', 'sprintf', 'snprintf', |
| 26 | + 'fscanf', 'scanf', 'sscanf', |
| 27 | + 'vfprintf', 'vprintf', 'vsprintf', 'vsnprintf', |
| 28 | + 'vfscanf', 'vscanf', 'vsscanf', |
| 29 | + # System / process |
| 30 | + 'system', |
| 31 | + 'atexit', |
| 32 | + 'raise', 'signal', |
| 33 | + # Time |
| 34 | + 'time', 'mktime', 'clock', |
| 35 | + # String-to-number conversion |
| 36 | + 'strtol', 'strtoul', 'strtoll', 'strtoull', |
| 37 | + 'strtof', 'strtod', 'strtold', |
| 38 | +] |
| 39 | + |
| 40 | + |
| 41 | +def ids(): |
| 42 | + return ('MISRA12_DIR_4.7', 'MISRA23_DIR_4.7', 'MISRA25_DIR_4.7', 'CPP_F073') |
| 43 | + |
| 44 | + |
| 45 | +def name(id): |
| 46 | + return { |
| 47 | + 'MISRA12_DIR_4.7': 'Published Standards/MISRA C 2012/' + """\ |
| 48 | +Directive 4.7 If a function returns error information, then that error information shall be tested""", |
| 49 | + 'MISRA23_DIR_4.7': 'Published Standards/MISRA C 2023/' + """\ |
| 50 | +Directive 4.7 If a function returns error information, then that error information shall be tested""", |
| 51 | + 'MISRA25_DIR_4.7': 'Published Standards/MISRA C 2025/' + """\ |
| 52 | +Directive 4.7 If a function returns error information, then that error information shall be tested""", |
| 53 | + 'CPP_F073': 'All Checks/Language Specific/C and C++/Functions/' + """\ |
| 54 | +If a function returns error information, then that error information shall be tested""", |
| 55 | + }[id] |
| 56 | + |
| 57 | + |
| 58 | +def tags(id): |
| 59 | + return { |
| 60 | + 'MISRA12_DIR_4.7': [ |
| 61 | + 'Language: C', |
| 62 | + 'Language: C++', |
| 63 | + 'Standard: MISRA C 2012', |
| 64 | + 'Category: Required', |
| 65 | + 'Functions', |
| 66 | + ], |
| 67 | + 'MISRA23_DIR_4.7': [ |
| 68 | + 'Language: C', |
| 69 | + 'Language: C++', |
| 70 | + 'Standard: MISRA C 2023', |
| 71 | + 'Category: Required', |
| 72 | + 'Functions', |
| 73 | + ], |
| 74 | + 'MISRA25_DIR_4.7': [ |
| 75 | + 'Language: C', |
| 76 | + 'Language: C++', |
| 77 | + 'Standard: MISRA C 2025', |
| 78 | + 'Category: Required', |
| 79 | + 'Functions', |
| 80 | + ], |
| 81 | + 'CPP_F073': [ |
| 82 | + 'Language: C', |
| 83 | + 'Language: C++', |
| 84 | + 'Functions', |
| 85 | + ], |
| 86 | + }.get(id) |
| 87 | + |
| 88 | + |
| 89 | +def detailed_description(id): |
| 90 | + return """\ |
| 91 | +<p><b>Amplification</b></p> |
| 92 | +<p>The list of functions that are deemed to return error information shall be determined by the project.</p> |
| 93 | +<p>The error information returned by a function shall be tested in a meaningful manner.</p> |
| 94 | + |
| 95 | +<p><b>Rationale</b></p> |
| 96 | +<p>A function (whether it is part of The Standard Library, a third party library or a user defined function) may be deemed to provide some means of indicating the occurrence of an error. This may be via an error flag, some special return value or some other means. Whenever such a mechanism is provided by a function the calling program shall check for the indication of an error as soon as the function returns.</p> |
| 97 | +<p>However, note that the checking of input values to functions is considered a more robust means of error prevention than trying to detect errors after the function has completed (see Dir 4.11).</p> |
| 98 | + |
| 99 | +<p><b>Exception</b></p> |
| 100 | +<p>If it can be shown, for example by checking arguments, that a function cannot return an error indication then there is no need to perform a check.</p> |
| 101 | + |
| 102 | +<p><b>See also</b></p> |
| 103 | +<p>Dir 4.11, Rule 17.7</p> |
| 104 | +""" |
| 105 | + |
| 106 | + |
| 107 | +def test_language(language): |
| 108 | + return language == 'C++' |
| 109 | + |
| 110 | + |
| 111 | +def test_entity(file): |
| 112 | + return file.kind().check('Code File, Header File') |
| 113 | + |
| 114 | + |
| 115 | +def test_global(): |
| 116 | + return False |
| 117 | + |
| 118 | + |
| 119 | +def define_options(check): |
| 120 | + check.option().checkbox('useDefaults', |
| 121 | + 'Treat common standard-library error-returning functions (malloc, fopen, etc.) as in-scope', |
| 122 | + True) |
| 123 | + check.option().text('extraFuncs', |
| 124 | + 'Additional error-returning function names (comma-separated)', |
| 125 | + '') |
| 126 | + |
| 127 | + |
| 128 | +def _build_func_set(check): |
| 129 | + funcs = set() |
| 130 | + if check.option().lookup('useDefaults'): |
| 131 | + funcs.update(DEFAULT_ERROR_FUNCS) |
| 132 | + extra = check.option().lookup('extraFuncs') or '' |
| 133 | + for name in extra.split(','): |
| 134 | + name = name.strip() |
| 135 | + if name: |
| 136 | + funcs.add(name) |
| 137 | + return funcs |
| 138 | + |
| 139 | + |
| 140 | +def _start_of_callee(name_lex): |
| 141 | + # Walk back through any member-access / scope-resolution chain |
| 142 | + # (a.b.foo(), ns::foo(), this->foo(), ...) and return the leftmost |
| 143 | + # lexeme that is part of the callee expression. |
| 144 | + lex = name_lex |
| 145 | + while True: |
| 146 | + prev = lex.previous(True, True) |
| 147 | + if prev is None or prev.text() not in ('.', '->', '::'): |
| 148 | + return lex |
| 149 | + prev2 = prev.previous(True, True) |
| 150 | + if prev2 is None or prev2.token() != 'Identifier': |
| 151 | + return lex |
| 152 | + lex = prev2 |
| 153 | + |
| 154 | + |
| 155 | +def _match_close_paren(open_lex): |
| 156 | + depth = 1 |
| 157 | + lex = open_lex.next(True, True) |
| 158 | + while lex: |
| 159 | + t = lex.text() |
| 160 | + if t == '(': |
| 161 | + depth += 1 |
| 162 | + elif t == ')': |
| 163 | + depth -= 1 |
| 164 | + if depth == 0: |
| 165 | + return lex |
| 166 | + lex = lex.next(True, True) |
| 167 | + return None |
| 168 | + |
| 169 | + |
| 170 | +def _matching_open_paren(close_paren): |
| 171 | + depth = 1 |
| 172 | + lex = close_paren.previous(True, True) |
| 173 | + while lex: |
| 174 | + t = lex.text() |
| 175 | + if t == ')': |
| 176 | + depth += 1 |
| 177 | + elif t == '(': |
| 178 | + depth -= 1 |
| 179 | + if depth == 0: |
| 180 | + return lex |
| 181 | + lex = lex.previous(True, True) |
| 182 | + return None |
| 183 | + |
| 184 | + |
| 185 | +def _is_control_header_close(close_paren): |
| 186 | + # True iff `close_paren` ends an if / while / for / switch header, |
| 187 | + # so the next statement is the controlled body. |
| 188 | + open_paren = _matching_open_paren(close_paren) |
| 189 | + if open_paren is None: |
| 190 | + return False |
| 191 | + kw = open_paren.previous(True, True) |
| 192 | + return (kw is not None and kw.token() == 'Keyword' |
| 193 | + and kw.text() in ('if', 'while', 'for', 'switch')) |
| 194 | + |
| 195 | + |
| 196 | +def _is_stmt_left_boundary(lex): |
| 197 | + # Token immediately to the left of a fresh statement. |
| 198 | + if lex is None: |
| 199 | + return False |
| 200 | + txt = lex.text() |
| 201 | + if txt in (';', '{', '}'): |
| 202 | + return True |
| 203 | + if lex.token() == 'Keyword' and txt in ('else', 'do'): |
| 204 | + return True |
| 205 | + if txt == ')': |
| 206 | + if _is_control_header_close(lex): |
| 207 | + return True |
| 208 | + # Otherwise the parens enclose a C-style cast like (T) or |
| 209 | + # (T *). The `(void)` cast is the explicit-discard idiom |
| 210 | + # (Rule 17.7) and signals intentional discard, so leave it |
| 211 | + # alone. Other casts are transparent — recurse onto the |
| 212 | + # token before the cast's opening '('. |
| 213 | + open_paren = _matching_open_paren(lex) |
| 214 | + if open_paren is None: |
| 215 | + return False |
| 216 | + inner = open_paren.next(True, True) |
| 217 | + if (inner is not None and inner.token() == 'Keyword' |
| 218 | + and inner.text() == 'void'): |
| 219 | + after_void = inner.next(True, True) |
| 220 | + if after_void is not None and after_void.text() == ')': |
| 221 | + return False |
| 222 | + return _is_stmt_left_boundary(open_paren.previous(True, True)) |
| 223 | + return False |
| 224 | + |
| 225 | + |
| 226 | +def _is_discarded_call(lexer, ref, ent): |
| 227 | + name_lex = lexer.lexeme(ref.line(), ref.column()) |
| 228 | + if name_lex is None: |
| 229 | + return False |
| 230 | + # Macro expansions resolve to a Call ref at the macro-use site, where the |
| 231 | + # lexer sees the macro identifier rather than the function name. Don't |
| 232 | + # report a use-site discard for a call that lives inside the macro body. |
| 233 | + if name_lex.text() != ent.name(): |
| 234 | + return False |
| 235 | + |
| 236 | + callee_start = _start_of_callee(name_lex) |
| 237 | + prev = callee_start.previous(True, True) |
| 238 | + if not _is_stmt_left_boundary(prev): |
| 239 | + return False |
| 240 | + |
| 241 | + open_paren = name_lex.next(True, True) |
| 242 | + if open_paren is None or open_paren.text() != '(': |
| 243 | + return False |
| 244 | + close_paren = _match_close_paren(open_paren) |
| 245 | + if close_paren is None: |
| 246 | + return False |
| 247 | + |
| 248 | + after = close_paren.next(True, True) |
| 249 | + if after is None: |
| 250 | + return False |
| 251 | + return after.text() == ';' |
| 252 | + |
| 253 | + |
| 254 | +def check(check, file): |
| 255 | + funcs = _build_func_set(check) |
| 256 | + if not funcs: |
| 257 | + return |
| 258 | + |
| 259 | + lexer = None |
| 260 | + for ref in file.filerefs('Call', 'Function ~Member'): |
| 261 | + ent = ref.ent() |
| 262 | + if ent.name() not in funcs: |
| 263 | + continue |
| 264 | + |
| 265 | + if lexer is None: |
| 266 | + lexer = file.lexer(lookup_ents=False) |
| 267 | + if lexer is None: |
| 268 | + return |
| 269 | + |
| 270 | + if _is_discarded_call(lexer, ref, ent): |
| 271 | + check.violation(ent, file, ref.line(), ref.column(), |
| 272 | + ERR1, ent.name()) |
0 commit comments