Skip to content

Commit 663bdd6

Browse files
author
Max Schaefer
authored
Merge pull request #396 from esben-semmle/js/unconditional-property-override
JS: add query: js/unconditional-property-override
2 parents 0cb09b1 + 2033bf8 commit 663bdd6

File tree

16 files changed

+478
-60
lines changed

16 files changed

+478
-60
lines changed

change-notes/1.19/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
| Replacement of a substring with itself (`js/identity-replacement`) | correctness, security, external/cwe/cwe-116 | Highlights string replacements that replace a string with itself, which usually indicates a mistake. Results shown on LGTM by default. |
2424
| Stored cross-site scripting (`js/stored-xss`) | security, external/cwe/cwe-079, external/cwe/cwe-116 | Highlights uncontrolled stored values flowing into HTML content, indicating a violation of [CWE-079](https://cwe.mitre.org/data/definitions/79.html). Results shown on LGTM by default. |
2525
| Unclear precedence of nested operators (`js/unclear-operator-precedence`) | maintainability, correctness, external/cwe/cwe-783 | Highlights nested binary operators whose relative precedence is easy to misunderstand. Results shown on LGTM by default. |
26+
| Useless assignment to property | maintainability | Highlights property assignments whose value is always overwritten. Results are shown on LGTM by default. |
2627
| User-controlled data in file | security, external/cwe/cwe-912 | Highlights locations where user-controlled data is written to a file. Results are not shown on LGTM by default. |
2728

2829
## Changes to existing queries

javascript/config/suites/javascript/maintainability-more

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
+ semmlecode-javascript-queries/Comments/TodoComments.ql: /Maintainability/Comments
33
+ semmlecode-javascript-queries/Declarations/DeadStoreOfGlobal.ql: /Maintainability/Declarations
44
+ semmlecode-javascript-queries/Declarations/DeadStoreOfLocal.ql: /Maintainability/Declarations
5+
+ semmlecode-javascript-queries/Declarations/DeadStoreOfProperty.ql: /Maintainability/Declarations
56
+ semmlecode-javascript-queries/Declarations/DuplicateVarDecl.ql: /Maintainability/Declarations
67
+ semmlecode-javascript-queries/Declarations/UnusedParameter.ql: /Maintainability/Declarations
78
+ semmlecode-javascript-queries/Declarations/UnusedVariable.ql: /Maintainability/Declarations
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
A value is assigned to a variable or property, but either that location is never read
8+
later on, or its value is always overwritten before being read. This means
9+
that the original assignment has no effect, and could indicate a logic error or
10+
incomplete code.
11+
</p>
12+
13+
</overview>
14+
<recommendation>
15+
16+
<p>
17+
Ensure that you check the control and data flow in the method carefully.
18+
If a value is really not needed, consider omitting the assignment. Be careful,
19+
though: if the right-hand side has a side-effect (like performing a method call),
20+
it is important to keep this to preserve the overall behavior.
21+
</p>
22+
23+
</recommendation>
24+
<example>
25+
26+
<p>
27+
In the following example, the return value of the call to <code>send</code> on line 2
28+
is assigned to the local variable <code>result</code>, but then never used.
29+
</p>
30+
31+
<sample src="examples/DeadStoreOfLocal.js" />
32+
33+
<p>
34+
Assuming that <code>send</code> returns a status code indicating whether the operation
35+
succeeded or not, the value of <code>result</code> should be checked, perhaps like this:
36+
</p>
37+
38+
<sample src="examples/DeadStoreOfLocalGood.js" />
39+
40+
</example>
41+
<references>
42+
43+
44+
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Dead_store">Dead store</a>.</li>
45+
46+
47+
48+
</references>
49+
</qhelp>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Provides classes and predicates for reasoning about dead stores.
3+
*/
4+
5+
import javascript
6+
7+
/**
8+
* Holds if `e` is an expression that may be used as a default initial value,
9+
* such as `0` or `-1`, or an empty object or array literal.
10+
*/
11+
predicate isDefaultInit(Expr e) {
12+
// primitive default values: zero, false, empty string, and (integer) -1
13+
e.(NumberLiteral).getValue().toFloat() = 0.0 or
14+
e.(NegExpr).getOperand().(NumberLiteral).getValue() = "1" or
15+
e.(ConstantString).getStringValue() = "" or
16+
e.(BooleanLiteral).getValue() = "false" or
17+
// initialising to an empty array or object literal, even if unnecessary,
18+
// can convey useful type information to the reader
19+
e.(ArrayExpr).getSize() = 0 or
20+
e.(ObjectExpr).getNumProperty() = 0 or
21+
SyntacticConstants::isNullOrUndefined(e)
22+
}
Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,9 @@
11
<!DOCTYPE qhelp PUBLIC
2-
"-//Semmle//qhelp//EN"
3-
"qhelp.dtd">
4-
<qhelp>
5-
<overview>
6-
<p>
7-
A value is assigned to a local variable, but either that variable is never read
8-
later on, or its value is always overwritten before being read. This means
9-
that the original assignment has no effect, and could indicate a logic error or
10-
incomplete code.
11-
</p>
12-
13-
</overview>
14-
<recommendation>
15-
16-
<p>
17-
Ensure that you check the control and data flow in the method carefully.
18-
If a value is really not needed, consider omitting the assignment. Be careful,
19-
though: if the right-hand side has a side-effect (like performing a method call),
20-
it is important to keep this to preserve the overall behavior.
21-
</p>
22-
23-
</recommendation>
24-
<example>
25-
26-
<p>
27-
In the following example, the return value of the call to <code>send</code> on line 2
28-
is assigned to the local variable <code>result</code>, but then never used.
29-
</p>
30-
31-
<sample src="examples/DeadStoreOfLocal.js" />
32-
33-
<p>
34-
Assuming that <code>send</code> returns a status code indicating whether the operation
35-
succeeded or not, the value of <code>result</code> should be checked, perhaps like this:
36-
</p>
37-
38-
<sample src="examples/DeadStoreOfLocalGood.js" />
39-
40-
</example>
41-
<references>
42-
43-
44-
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Dead_store">Dead store</a>.</li>
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
454

5+
<qhelp>
466

7+
<include src="DeadStore.qhelp" />
478

48-
</references>
499
</qhelp>

javascript/ql/src/Declarations/DeadStoreOfLocal.ql

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import javascript
14+
import DeadStore
1415

1516
/**
1617
* Holds if `vd` is a definition of variable `v` that is dead, that is,
@@ -25,22 +26,6 @@ predicate deadStoreOfLocal(VarDef vd, PurelyLocalVariable v) {
2526
not exists (SsaExplicitDefinition ssa | ssa.defines(vd, v))
2627
}
2728

28-
/**
29-
* Holds if `e` is an expression that may be used as a default initial value,
30-
* such as `0` or `-1`, or an empty object or array literal.
31-
*/
32-
predicate isDefaultInit(Expr e) {
33-
// primitive default values: zero, false, empty string, and (integer) -1
34-
e.(NumberLiteral).getValue().toFloat() = 0.0 or
35-
e.(NegExpr).getOperand().(NumberLiteral).getValue() = "1" or
36-
e.(ConstantString).getStringValue() = "" or
37-
e.(BooleanLiteral).getValue() = "false" or
38-
// initialising to an empty array or object literal, even if unnecessary,
39-
// can convey useful type information to the reader
40-
e.(ArrayExpr).getSize() = 0 or
41-
e.(ObjectExpr).getNumProperty() = 0
42-
}
43-
4429
from VarDef dead, PurelyLocalVariable v // captured variables may be read by closures, so don't flag them
4530
where deadStoreOfLocal(dead, v) and
4631
// the variable should be accessed somewhere; otherwise it will be flagged by UnusedVariable
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
5+
<qhelp>
6+
7+
<include src="DeadStore.qhelp" />
8+
9+
</qhelp>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* @name Useless assignment to property
3+
* @description An assignment to a property whose value is always overwritten has no effect.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id js/useless-assignment-to-property
7+
* @tags maintainability
8+
* @precision high
9+
*/
10+
11+
import javascript
12+
import Expressions.DOMProperties
13+
import DeadStore
14+
15+
/**
16+
* Holds if `write` writes to property `name` of `base`, and `base` is the only base object of `write`.
17+
*/
18+
predicate unambiguousPropWrite(DataFlow::SourceNode base, string name, DataFlow::PropWrite write) {
19+
write = base.getAPropertyWrite(name) and
20+
not exists (DataFlow::SourceNode otherBase |
21+
otherBase != base and
22+
write = otherBase.getAPropertyWrite(name)
23+
)
24+
}
25+
26+
/**
27+
* Holds if `assign1` and `assign2` both assign property `name` of the same object, and `assign2` post-dominates `assign1`.
28+
*/
29+
predicate postDominatedPropWrite(string name, DataFlow::PropWrite assign1, DataFlow::PropWrite assign2) {
30+
exists (ControlFlowNode write1, ControlFlowNode write2, DataFlow::SourceNode base, ReachableBasicBlock block1, ReachableBasicBlock block2 |
31+
write1 = assign1.getWriteNode() and
32+
write2 = assign2.getWriteNode() and
33+
block1 = write1.getBasicBlock() and
34+
block2 = write2.getBasicBlock() and
35+
unambiguousPropWrite(base, name, assign1) and
36+
unambiguousPropWrite(base, name, assign2) and
37+
block2.postDominates(block1) and
38+
(block1 = block2 implies
39+
exists (int i1, int i2 |
40+
write1 = block1.getNode(i1) and
41+
write2 = block2.getNode(i2) and
42+
i1 < i2
43+
)
44+
)
45+
)
46+
}
47+
48+
/**
49+
* Holds if `e` may access a property named `name`.
50+
*/
51+
bindingset[name]
52+
predicate maybeAccessesProperty(Expr e, string name) {
53+
(e.(PropAccess).getPropertyName() = name and e instanceof RValue) or
54+
// conservatively reject all side-effects
55+
e.isImpure()
56+
}
57+
58+
/**
59+
* Holds if `assign1` and `assign2` both assign property `name`, but `assign1` is dead because of `assign2`.
60+
*/
61+
predicate isDeadAssignment(string name, DataFlow::PropWrite assign1, DataFlow::PropWrite assign2) {
62+
postDominatedPropWrite(name, assign1, assign2) and
63+
noPropAccessBetween(name, assign1, assign2) and
64+
not isDOMProperty(name)
65+
}
66+
67+
/**
68+
* Holds if `assign` assigns a property `name` that may be accessed somewhere else in the same block,
69+
* `after` indicates if the access happens before or after the node for `assign`.
70+
*/
71+
bindingset[name]
72+
predicate maybeAccessesAssignedPropInBlock(string name, DataFlow::PropWrite assign, boolean after) {
73+
exists (ControlFlowNode write, ReachableBasicBlock block, int i, int j, Expr e |
74+
write = assign.getWriteNode() and
75+
block = assign.getBasicBlock() and
76+
write = block.getNode(i) and
77+
e = block.getNode(j) and
78+
maybeAccessesProperty(e, name) |
79+
after = true and i < j
80+
or
81+
after = false and j < i
82+
)
83+
}
84+
85+
/**
86+
* Holds if `assign1` and `assign2` both assign property `name`, and the assigned property is not accessed between the two assignments.
87+
*/
88+
bindingset[name]
89+
predicate noPropAccessBetween(string name, DataFlow::PropWrite assign1, DataFlow::PropWrite assign2) {
90+
exists (ControlFlowNode write1, ControlFlowNode write2, ReachableBasicBlock block1, ReachableBasicBlock block2 |
91+
write1 = assign1.getWriteNode() and
92+
write2 = assign2.getWriteNode() and
93+
write1.getBasicBlock() = block1 and
94+
write2.getBasicBlock() = block2 and
95+
if block1 = block2 then
96+
// same block: check for access between
97+
not exists (int i1, Expr mid, int i2 |
98+
assign1.getWriteNode() = block1.getNode(i1) and
99+
assign2.getWriteNode() = block2.getNode(i2) and
100+
mid = block1.getNode([i1+1..i2-1]) and
101+
maybeAccessesProperty(mid, name)
102+
)
103+
else
104+
// other block:
105+
not (
106+
// check for an access after the first write node
107+
maybeAccessesAssignedPropInBlock(name, assign1, true) or
108+
// check for an access between the two write blocks
109+
exists (ReachableBasicBlock mid |
110+
block1.getASuccessor+() = mid and
111+
mid.getASuccessor+() = block2 |
112+
maybeAccessesProperty(mid.getANode(), name)
113+
) or
114+
// check for an access before the second write node
115+
maybeAccessesAssignedPropInBlock(name, assign2, false)
116+
)
117+
)
118+
}
119+
120+
from string name, DataFlow::PropWrite assign1, DataFlow::PropWrite assign2
121+
where isDeadAssignment(name, assign1, assign2) and
122+
// whitelist
123+
not (
124+
// Google Closure Compiler pattern: `o.p = o['p'] = v`
125+
exists (PropAccess p1, PropAccess p2 |
126+
p1 = assign1.getAstNode() and
127+
p2 = assign2.getAstNode() |
128+
p1 instanceof DotExpr and p2 instanceof IndexExpr
129+
or
130+
p2 instanceof DotExpr and p1 instanceof IndexExpr
131+
)
132+
or
133+
// don't flag overwrites for default values
134+
isDefaultInit(assign1.getRhs().asExpr().getUnderlyingValue())
135+
or
136+
// don't flag assignments in externs
137+
assign1.getAstNode().inExternsFile()
138+
or
139+
// exclude result from js/overwritten-property
140+
assign2.getBase() instanceof DataFlow::ObjectLiteralNode
141+
)
142+
select assign1.getWriteNode(), "This write to property '" + name + "' is useless, since $@ always overrides it.", assign2.getWriteNode(), "another property write"

0 commit comments

Comments
 (0)