JS: Add StringOps::RegExpTest

This commit is contained in:
Asger Feldthaus
2020-06-01 07:47:06 +01:00
parent 7265e94028
commit fa1a6eefa7
4 changed files with 220 additions and 0 deletions

View File

@@ -629,4 +629,143 @@ module StringOps {
class HtmlConcatenationLeaf extends ConcatenationLeaf {
HtmlConcatenationLeaf() { getRoot() instanceof HtmlConcatenationRoot }
}
/**
* A data flow node whose boolean value indicates whether a regexp matches a given string.
*
* For example, the condition of each of the following `if`-statements are `RegExpTest` nodes:
* ```js
* if (regexp.test(str)) { ... }
* if (regexp.exec(str) != null) { ... }
* if (str.matches(regexp)) { ... }
* ```
*
* Note that `RegExpTest` represents a boolean-valued expression or one
* that is coerced to a boolean, which is not always the same as the call that performs the
* regexp-matching. For example, the `exec` call below is not itself a `RegExpTest`,
* but the `match` variable in the condition is:
* ```js
* let match = regexp.exec(str);
* if (!match) { ... } // <--- 'match' is the RegExpTest
* ```
*/
class RegExpTest extends DataFlow::Node {
RegExpTest::Range range;
RegExpTest() { this = range }
/**
* Gets the AST of the regular expression used in the test, if it can be seen locally.
*/
RegExpTerm getRegExp() {
result = getRegExpOperand().getALocalSource().(DataFlow::RegExpCreationNode).getRoot()
or
result = range.getRegExpOperand(true).asExpr().(StringLiteral).asRegExp()
}
/**
* Gets the data flow node corresponding to the regular expression object used in the test.
*
* In some cases this represents a string value being coerced to a RegExp object.
*/
DataFlow::Node getRegExpOperand() { result = range.getRegExpOperand(_) }
/**
* Gets the data flow node corresponding to the string being tested against the regular expression.
*/
DataFlow::Node getStringOperand() { result = range.getStringOperand() }
/**
* Gets the return value indicating that the string matched the regular expression.
*
* For example, for `regexp.exec(str) == null`, the polarity is `false`, and for
* `regexp.exec(str) != null` the polarity is `true`.
*/
boolean getPolarity() { result = range.getPolarity() }
}
/**
* Companion module to the `RegExpTest` class.
*/
module RegExpTest {
/**
* A data flow node whose boolean value indicates whether a regexp matches a given string.
*
* This class can be extended to contribute new kinds of `RegExpTest` nodes.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the data flow node corresponding to the regular expression object used in the test.
*/
abstract DataFlow::Node getRegExpOperand(boolean coerced);
/**
* Gets the data flow node corresponding to the string being tested against the regular expression.
*/
abstract DataFlow::Node getStringOperand();
/**
* Gets the return value indicating that the string matched the regular expression.
*/
boolean getPolarity() { result = true }
}
private class TestCall extends Range, DataFlow::MethodCallNode {
TestCall() { getMethodName() = "test" }
override DataFlow::Node getRegExpOperand(boolean coerced) { result = getReceiver() and coerced = false }
override DataFlow::Node getStringOperand() { result = getArgument(0) }
}
private class MatchesCall extends Range, DataFlow::MethodCallNode {
MatchesCall() { getMethodName() = "matches" }
override DataFlow::Node getRegExpOperand(boolean coerced) { result = getArgument(0) and coerced = true }
override DataFlow::Node getStringOperand() { result = getReceiver() }
}
private class ExecCall extends DataFlow::MethodCallNode {
ExecCall() { getMethodName() = "exec" }
}
predicate isCoercedToBoolean(Expr e) {
e = any(ConditionGuardNode guard).getTest()
or
e = any(LogNotExpr n).getOperand()
}
/**
* Holds if `e` evaluating to `polarity` implies that `operand` is not null.
*/
private predicate impliesNotNull(Expr e, Expr operand, boolean polarity) {
exists(EqualityTest test |
e = test and
polarity = test.getPolarity().booleanNot() and
test.hasOperands(any(NullLiteral n), operand)
)
or
isCoercedToBoolean(e) and
operand = e and
polarity = true
}
private class ExecTest extends Range, DataFlow::ValueNode {
ExecCall exec;
boolean polarity;
ExecTest() {
exists(Expr use | exec.flowsToExpr(use) |
impliesNotNull(astNode, use, polarity)
)
}
override DataFlow::Node getRegExpOperand(boolean coerced) { result = exec.getReceiver() and coerced = false }
override DataFlow::Node getStringOperand() { result = exec.getArgument(0) }
override boolean getPolarity() { result = polarity }
}
}
}

View File

@@ -0,0 +1,36 @@
regexpTest
| tst.js:6:9:6:28 | /^[a-z]+$/.test(str) |
| tst.js:7:9:7:36 | /^[a-z] ... != null |
| tst.js:8:9:8:28 | /^[a-z]+$/.exec(str) |
| tst.js:9:9:9:31 | str.mat ... -z]+$/) |
| tst.js:10:9:10:31 | str.mat ... -z]+$") |
| tst.js:12:9:12:24 | regexp.test(str) |
| tst.js:13:9:13:32 | regexp. ... != null |
| tst.js:14:9:14:24 | regexp.exec(str) |
| tst.js:15:9:15:27 | str.matches(regexp) |
| tst.js:18:9:18:13 | match |
| tst.js:19:10:19:14 | match |
| tst.js:20:9:20:21 | match == null |
| tst.js:21:9:21:21 | match != null |
| tst.js:22:9:22:13 | match |
| tst.js:25:23:25:27 | match |
| tst.js:29:21:29:36 | regexp.test(str) |
| tst.js:33:21:33:39 | str.matches(regexp) |
#select
| tst.js:6:9:6:28 | /^[a-z]+$/.test(str) | tst.js:6:10:6:17 | ^[a-z]+$ | tst.js:6:9:6:18 | /^[a-z]+$/ | tst.js:6:25:6:27 | str | true |
| tst.js:7:9:7:36 | /^[a-z] ... != null | tst.js:7:10:7:17 | ^[a-z]+$ | tst.js:7:9:7:18 | /^[a-z]+$/ | tst.js:7:25:7:27 | str | true |
| tst.js:8:9:8:28 | /^[a-z]+$/.exec(str) | tst.js:8:10:8:17 | ^[a-z]+$ | tst.js:8:9:8:18 | /^[a-z]+$/ | tst.js:8:25:8:27 | str | true |
| tst.js:9:9:9:31 | str.mat ... -z]+$/) | tst.js:9:22:9:29 | ^[a-z]+$ | tst.js:9:21:9:30 | /^[a-z]+$/ | tst.js:9:9:9:11 | str | true |
| tst.js:10:9:10:31 | str.mat ... -z]+$") | tst.js:10:22:10:29 | ^[a-z]+$ | tst.js:10:21:10:30 | "^[a-z]+$" | tst.js:10:9:10:11 | str | true |
| tst.js:12:9:12:24 | regexp.test(str) | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:12:9:12:14 | regexp | tst.js:12:21:12:23 | str | true |
| tst.js:13:9:13:32 | regexp. ... != null | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:13:9:13:14 | regexp | tst.js:13:21:13:23 | str | true |
| tst.js:14:9:14:24 | regexp.exec(str) | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:14:9:14:14 | regexp | tst.js:14:21:14:23 | str | true |
| tst.js:15:9:15:27 | str.matches(regexp) | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:15:21:15:26 | regexp | tst.js:15:9:15:11 | str | true |
| tst.js:18:9:18:13 | match | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | true |
| tst.js:19:10:19:14 | match | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | true |
| tst.js:20:9:20:21 | match == null | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | false |
| tst.js:21:9:21:21 | match != null | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | true |
| tst.js:22:9:22:13 | match | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | true |
| tst.js:25:23:25:27 | match | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:17:17:17:22 | regexp | tst.js:17:29:17:31 | str | true |
| tst.js:29:21:29:36 | regexp.test(str) | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:29:21:29:26 | regexp | tst.js:29:33:29:35 | str | true |
| tst.js:33:21:33:39 | str.matches(regexp) | tst.js:3:17:3:24 | ^[a-z]+$ | tst.js:33:33:33:38 | regexp | tst.js:33:21:33:23 | str | true |

View File

@@ -0,0 +1,6 @@
import javascript
query StringOps::RegExpTest regexpTest() { any() }
from StringOps::RegExpTest test
select test, test.getRegExp(), test.getRegExpOperand(), test.getStringOperand(), test.getPolarity()

View File

@@ -0,0 +1,39 @@
import 'dummy';
const regexp = /^[a-z]+$/;
function f(str) {
if (/^[a-z]+$/.test(str)) {}
if (/^[a-z]+$/.exec(str) != null) {}
if (/^[a-z]+$/.exec(str)) {}
if (str.matches(/^[a-z]+$/)) {}
if (str.matches("^[a-z]+$")) {}
if (regexp.test(str)) {}
if (regexp.exec(str) != null) {}
if (regexp.exec(str)) {}
if (str.matches(regexp)) {}
let match = regexp.exec(str);
if (match) {}
if (!match) {}
if (match == null) {}
if (match != null) {}
if (match && match[1] == "") {}
something({
someOption: !!match
});
something({
someOption: regexp.test(str)
});
something({
someOption: str.matches(regexp)
});
something({
someOption: regexp.exec(str) // not recognized as RegExpTest
})
}