Skip to content

Commit a2c9912

Browse files
committed
RULE-7-0-6 - NumericAssignmentTypeMismatch
Detects inappropriate assignments between numeric types that violate type compatibility requirements, including implicit conversions that may cause information loss or unexpected behavior changes. [a]
1 parent b62394a commit a2c9912

File tree

4 files changed

+541
-0
lines changed

4 files changed

+541
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* @id cpp/misra/numeric-assignment-type-mismatch
3+
* @name RULE-7-0-6: Assignment between numeric types shall be appropriate
4+
* @description Assignment between numeric types with different sizes, signedness, or type
5+
* categories can lead to unexpected information loss, undefined behavior, or silent
6+
* overload resolution changes.
7+
* @kind problem
8+
* @precision high
9+
* @problem.severity error
10+
* @tags external/misra/id/rule-7-0-6
11+
* scope/single-translation-unit
12+
* external/misra/enforcement/decidable
13+
* external/misra/obligation/required
14+
*/
15+
16+
import cpp
17+
import codingstandards.cpp.misra
18+
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
19+
20+
/**
21+
* The signedness of a numeric type.
22+
*/
23+
newtype Signedness =
24+
Signed() or
25+
Unsigned()
26+
27+
/**
28+
* The type category of a numeric type - either integral or floating-point.
29+
*/
30+
newtype TypeCategory =
31+
Integral() or
32+
FloatingPoint()
33+
34+
/**
35+
* A numeric type is a type that represents a number, either an integral or a floating-point.
36+
*
37+
* In addition to the basic integral and floating-point types, it includes:
38+
* - Enum types with an explicit underlying type that is a numeric type.
39+
* - Typedef'd types that are numeric types.
40+
* - Numeric types with specifiers (e.g., `const`, `volatile`, `restrict`).
41+
*/
42+
class NumericType extends Type {
43+
Type realType;
44+
45+
NumericType() {
46+
realType = this.getUnspecifiedType().(IntegralType) or
47+
realType = this.getUnspecifiedType().(FloatingPointType) or
48+
realType = this.getUnspecifiedType().(Enum).getExplicitUnderlyingType().getUnspecifiedType()
49+
}
50+
51+
Signedness getSignedness() {
52+
if realType.(IntegralType).isUnsigned() then result = Unsigned() else result = Signed()
53+
}
54+
55+
TypeCategory getTypeCategory() {
56+
realType instanceof IntegralType and result = Integral()
57+
or
58+
realType instanceof FloatingPointType and result = FloatingPoint()
59+
}
60+
61+
float getUpperBound() { result = typeUpperBound(realType) }
62+
63+
float getLowerBound() { result = typeLowerBound(realType) }
64+
65+
Type getRealType() { result = realType }
66+
}
67+
68+
predicate isAssignment(Expr source, NumericType targetType, string context) {
69+
// Assignment operator
70+
exists(Assignment assign |
71+
assign.getRValue() = source and
72+
context = "assignment"
73+
|
74+
// TODO generalize to variable init (do we need this for bitfields?) and extract
75+
if isAssignedToBitfield(source, _)
76+
then
77+
exists(BitField bf |
78+
isAssignedToBitfield(source, bf) and
79+
targetType.(IntegralType).(NumericType).getSignedness() =
80+
bf.getType().(NumericType).getSignedness() and
81+
// smallest integral type that can hold the bit field value
82+
targetType.getSize() * 8 >= bf.getNumBits() and
83+
not exists(IntegralType other |
84+
other.getSize() * 8 >= bf.getNumBits() and
85+
other.(NumericType).getSignedness() = targetType.getSignedness() and
86+
other.getSize() < targetType.getSize()
87+
)
88+
)
89+
else targetType = assign.getLValue().getType()
90+
)
91+
or
92+
// Variable initialization
93+
exists(Variable v, Initializer init |
94+
init.getExpr() = source and
95+
v.getInitializer() = init and
96+
targetType = v.getType() and
97+
context = "initialization"
98+
)
99+
or
100+
exists(Call call, int i |
101+
call.getArgument(i) = source and
102+
not targetType.stripTopLevelSpecifiers() instanceof ReferenceType and
103+
context = "function argument"
104+
|
105+
targetType = call.getTarget().getParameter(i).getType()
106+
or
107+
// Handle varargs - use the fully converted type of the argument
108+
call.getTarget().getNumberOfParameters() <= i and
109+
targetType = source.getFullyConverted().getType()
110+
)
111+
or
112+
// Return statement
113+
exists(ReturnStmt ret, Function f |
114+
ret.getExpr() = source and
115+
ret.getEnclosingFunction() = f and
116+
targetType = f.getType() and
117+
not targetType.stripTopLevelSpecifiers() instanceof ReferenceType and
118+
context = "return"
119+
)
120+
or
121+
// Switch case
122+
exists(SwitchCase case, SwitchStmt switch |
123+
case.getExpr() = source and
124+
case.getSwitchStmt() = switch and
125+
targetType = switch.getExpr().getFullyConverted().getType() and
126+
context = "switch case"
127+
)
128+
}
129+
130+
predicate isAssignedToBitfield(Expr source, BitField bf) {
131+
exists(Assignment assign |
132+
assign.getRValue() = source and
133+
assign.getLValue() = bf.getAnAccess()
134+
)
135+
}
136+
137+
predicate isValidConstantAssignment(Expr source, NumericType targetType) {
138+
isAssignment(source, targetType, _) and
139+
// Source is an integer constant expression
140+
source.isConstant() and
141+
source.getType().(NumericType).getTypeCategory() = Integral() and
142+
exists(float val | val = source.getValue().toFloat() |
143+
// Bit field assignment: check if the value fits in the bit field
144+
exists(BitField bf, int numBits |
145+
isAssignedToBitfield(source, bf) and
146+
numBits = bf.getNumBits() and
147+
val >= 0 and
148+
val < 2.pow(numBits)
149+
)
150+
or
151+
// Regular assignment: check if the value fits in the target type range
152+
not isAssignedToBitfield(source, _) and
153+
targetType.getLowerBound() <= val and
154+
val <= targetType.getUpperBound()
155+
)
156+
}
157+
158+
predicate isValidTypeMatch(NumericType sourceType, NumericType targetType) {
159+
// Same type category, signedness and size
160+
sourceType.getTypeCategory() = targetType.getTypeCategory() and
161+
sourceType.getSignedness() = targetType.getSignedness() and
162+
sourceType.getSize() = targetType.getSize()
163+
}
164+
165+
predicate hasConstructorException(FunctionCall call) {
166+
exists(Constructor ctor, Class c |
167+
call.getTarget() = ctor and
168+
c = ctor.getDeclaringType() and
169+
// Constructor callable with single numeric argument
170+
ctor.getNumberOfParameters() = 1 and
171+
ctor.getParameter(0).getType() instanceof NumericType and
172+
// No other single-argument constructors except copy/move
173+
not exists(Constructor other |
174+
other.getDeclaringType() = c and
175+
other != ctor and
176+
other.getNumberOfParameters() = 1 and
177+
not other instanceof CopyConstructor and
178+
not other instanceof MoveConstructor
179+
)
180+
)
181+
}
182+
183+
/**
184+
* An id-expression that has a numeric type.
185+
*
186+
* This is restricted to variable accesses, that are not explicitly qualified in any way.
187+
*/
188+
class IdExpression extends VariableAccess {
189+
IdExpression() {
190+
// Not a member variable
191+
(
192+
not exists(this.getQualifier())
193+
or
194+
// Member variable, but the qualifier is not explicit
195+
this.getQualifier().isCompilerGenerated()
196+
) and
197+
// No name qualifiers
198+
not exists(NameQualifier qual | qual.getExpr() = this)
199+
}
200+
}
201+
202+
predicate isValidWidening(Expr source, NumericType sourceType, NumericType targetType) {
203+
isAssignment(source, targetType, _) and
204+
source.getType() = sourceType and
205+
// Same type category and signedness, source size smaller, source is id-expression or has constructor exception
206+
(
207+
source instanceof IdExpression or
208+
hasConstructorException(any(Call call | call.getAnArgument() = source))
209+
) and
210+
sourceType.getTypeCategory() = targetType.getTypeCategory() and
211+
sourceType.getSignedness() = targetType.getSignedness() and
212+
sourceType.getSize() < targetType.getSize()
213+
}
214+
215+
/**
216+
* A non-extensible call is a call that cannot be extended by adding new overloads.
217+
*/
218+
predicate isNonExtensible(Call c) {
219+
exists(NameQualifier qual | qual.getExpr() = c and c.getTarget() instanceof MemberFunction)
220+
or
221+
exists(c.getQualifier()) and not c.getQualifier().isCompilerGenerated()
222+
or
223+
c.getTarget() instanceof Operator
224+
}
225+
226+
predicate isOverloadIndependent(Call call, Expr arg) {
227+
arg = call.getAnArgument() and
228+
(
229+
// Call through function pointer
230+
call instanceof ExprCall
231+
or
232+
isNonExtensible(call) and
233+
forall(Function target, Function overload, int i |
234+
target = call.getTarget() and
235+
(
236+
overload = target.getAnOverload()
237+
or
238+
// Instantiated function templates don't directly participate in overload resolution
239+
// so check the templates overloads
240+
overload = target.(FunctionTemplateInstantiation).getTemplate().getAnOverload()
241+
) and
242+
overload.getNumberOfParameters() = call.getNumberOfArguments() and
243+
call.getArgument(i) = arg
244+
|
245+
// Check that the parameter types match
246+
overload.getParameter(i).getType().getUnspecifiedType() =
247+
target.getParameter(i).getType().getUnspecifiedType()
248+
)
249+
)
250+
}
251+
252+
/**
253+
* Check if the source expression should have the same type as the target type.
254+
*/
255+
predicate shouldHaveSameType(Expr source) {
256+
exists(Call call |
257+
call.getAnArgument() = source and
258+
isAssignment(source, _, _) and
259+
not hasConstructorException(call)
260+
|
261+
not isOverloadIndependent(call, source)
262+
or
263+
// Passed as a varargs parameter
264+
exists(int i |
265+
call.getTarget().isVarargs() and
266+
call.getArgument(i) = source and
267+
// Argument is greater than the number of parameters
268+
call.getTarget().getNumberOfParameters() <= i
269+
)
270+
)
271+
}
272+
273+
predicate isValidAssignment(Expr source, NumericType targetType, string context) {
274+
isAssignment(source, targetType, context) and
275+
exists(NumericType sourceType | sourceType = source.getType() |
276+
if shouldHaveSameType(source)
277+
then sourceType.getRealType() = targetType.getRealType()
278+
else (
279+
// Valid type match
280+
isValidTypeMatch(sourceType, targetType)
281+
or
282+
// Valid widening assignment
283+
isValidWidening(source, sourceType, targetType)
284+
or
285+
// Valid constant assignment (integer constants)
286+
isValidConstantAssignment(source, targetType)
287+
)
288+
)
289+
}
290+
291+
from Expr source, NumericType sourceType, NumericType targetType, string context
292+
where
293+
not isExcluded(source, ConversionsPackage::numericAssignmentTypeMismatchQuery()) and
294+
isAssignment(source, targetType, context) and
295+
// The assignment must be between numeric types
296+
sourceType = source.getType() and
297+
not isValidAssignment(source, targetType, context)
298+
select source,
299+
"Assignment between incompatible numeric types from '" + sourceType.getName() + "' to '" +
300+
targetType.getName() + "'."
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
| test.cpp:21:8:21:11 | 300 | Assignment between incompatible numeric types from 'unsigned int' to 'uint8_t'. |
2+
| test.cpp:24:9:24:12 | 0.0 | Assignment between incompatible numeric types from 'float' to 'double'. |
3+
| test.cpp:30:8:30:9 | g4 | Assignment between incompatible numeric types from 'int8_t' to 'uint8_t'. |
4+
| test.cpp:31:8:31:9 | g3 | Assignment between incompatible numeric types from 'uint8_t' to 'int8_t'. |
5+
| test.cpp:36:8:36:9 | g5 | Assignment between incompatible numeric types from 'uint16_t' to 'uint8_t'. |
6+
| test.cpp:37:8:37:9 | g7 | Assignment between incompatible numeric types from 'uint64_t' to 'uint16_t'. |
7+
| test.cpp:42:8:42:9 | g2 | Assignment between incompatible numeric types from 'int32_t' to 'float'. |
8+
| test.cpp:43:8:43:9 | g9 | Assignment between incompatible numeric types from 'float' to 'int32_t'. |
9+
| test.cpp:55:8:55:14 | ... + ... | Assignment between incompatible numeric types from 'int' to 'uint8_t'. |
10+
| test.cpp:56:21:56:27 | ... + ... | Assignment between incompatible numeric types from 'int' to 'int16_t'. |
11+
| test.cpp:67:11:67:13 | 32 | Assignment between incompatible numeric types from 'uint32_t' to 'unsigned char'. |
12+
| test.cpp:69:11:69:12 | g5 | Assignment between incompatible numeric types from 'uint16_t' to 'unsigned char'. |
13+
| test.cpp:81:8:81:9 | l1 | Assignment between incompatible numeric types from 'Colour' to 'uint8_t'. |
14+
| test.cpp:91:6:91:7 | g2 | Assignment between incompatible numeric types from 'int32_t' to 'int64_t'. |
15+
| test.cpp:94:6:94:7 | g8 | Assignment between incompatible numeric types from 'int64_t' to 'int32_t'. |
16+
| test.cpp:96:6:96:7 | l1 | Assignment between incompatible numeric types from 'int' to 'int32_t'. |
17+
| test.cpp:109:6:109:7 | g4 | Assignment between incompatible numeric types from 'int8_t' to 'int32_t'. |
18+
| test.cpp:118:6:118:6 | 2 | Assignment between incompatible numeric types from 'int' to 'long'. |
19+
| test.cpp:126:14:126:15 | g3 | Assignment between incompatible numeric types from 'uint8_t' to 'int'. |
20+
| test.cpp:138:6:138:7 | g1 | Assignment between incompatible numeric types from 'uint32_t' to 'size_t'. |
21+
| test.cpp:160:9:160:10 | 42 | Assignment between incompatible numeric types from 'int' to 'long'. |
22+
| test.cpp:189:23:189:24 | 42 | Assignment between incompatible numeric types from 'int' to 'unsigned long'. |
23+
| test.cpp:199:19:199:25 | ... + ... | Assignment between incompatible numeric types from 'int' to 'int16_t'. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rules/RULE-7-0-6/NumericAssignmentTypeMismatch.ql

0 commit comments

Comments
 (0)