Skip to content

Commit 0a2e59a

Browse files
Checks: Added new MISRA12_DIR_4.7: If a function returns error information, then that error information shall be tested [autosync]
1 parent 3454f22 commit 0a2e59a

5 files changed

Lines changed: 416 additions & 61 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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())
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
5+
extern void use(void *);
6+
extern void use_int(int);
7+
8+
void f(void)
9+
{
10+
void *p;
11+
int rc;
12+
FILE *fp;
13+
14+
// Statement-only call with discarded return: non-compliant.
15+
malloc(100); // UndCC_Violation
16+
fopen("a", "r"); // UndCC_Violation
17+
fclose(NULL); // UndCC_Violation
18+
printf("hi\n"); // UndCC_Violation
19+
fseek(NULL, 0, 0); // UndCC_Violation
20+
21+
// Result assigned: compliant — caller can subsequently test it.
22+
p = malloc(100); // UndCC_Valid
23+
fp = fopen("a", "r"); // UndCC_Valid
24+
rc = fclose(fp); // UndCC_Valid
25+
26+
// Result tested in a controlling expression: compliant.
27+
if (malloc(100) == NULL) { use_int(1); } // UndCC_Valid
28+
if (fopen("b", "r")) { use_int(2); } // UndCC_Valid
29+
while (fread(p, 1, 1, fp) == 1) { } // UndCC_Valid
30+
31+
// Result used in a wider expression: compliant.
32+
use(malloc(100)); // UndCC_Valid
33+
use_int(printf("x") + 1); // UndCC_Valid
34+
35+
// Cast-to-void: explicit-discard idiom — accepted as compliant
36+
// (Rule 17.7 deviation), though Dir 4.7 strictly requires testing.
37+
(void)malloc(100); // UndCC_Valid
38+
39+
// Cast to non-void with discarded result: still a violation.
40+
// The boundary check looks through the cast.
41+
(char *)malloc(100); // UndCC_Violation
42+
(FILE *)fopen("d", "r"); // UndCC_Violation
43+
44+
// Statement-only call as the body of an if/else without braces.
45+
if (rc) malloc(100); // UndCC_Violation
46+
else fopen("c", "r"); // UndCC_Violation
47+
48+
// Body of a do/while without braces.
49+
do malloc(100); while (0); // UndCC_Violation
50+
51+
// free returns void, never reported.
52+
free(p); // UndCC_Valid
53+
54+
// Functions outside the configured list are not flagged even if
55+
// their return value is discarded.
56+
strlen("test"); // UndCC_Valid
57+
58+
// Result used in a ternary: compliant — the call's value is the
59+
// expression result.
60+
p = (rc) ? malloc(100) : NULL; // UndCC_Valid
61+
62+
// for-loop init / update clauses: both discard the call's
63+
// result. Neither is flagged: the init's previous token is '('
64+
// and the update's following token is ')', so neither side of
65+
// the boundary check matches.
66+
for (malloc(100); rc < 1; rc++) { } // UndCC_FalseNeg
67+
for (rc = 0; rc < 1; malloc(100)) { } // UndCC_FalseNeg
68+
69+
// Labeled / case-labeled statement-only calls. The lexer-based
70+
// boundary check sees ':' before the call and treats it as
71+
// non-boundary (same token as a ternary ':'), so these are not
72+
// flagged.
73+
switch (rc) {
74+
case 1: malloc(100); break; // UndCC_FalseNeg
75+
default: break;
76+
}
77+
goto lbl;
78+
lbl: malloc(100); // UndCC_FalseNeg
79+
}
80+
81+
void *return_alloc(void)
82+
{
83+
// Result used as the return value: compliant.
84+
return malloc(100); // UndCC_Valid
85+
}

0 commit comments

Comments
 (0)