Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F1262398
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/lint/linter/xhpast/rules/__tests__/formatted-string/formatted-string.lint-test b/src/lint/linter/xhpast/rules/__tests__/formatted-string/formatted-string.lint-test
index 67ec7746..f9e5a11b 100644
--- a/src/lint/linter/xhpast/rules/__tests__/formatted-string/formatted-string.lint-test
+++ b/src/lint/linter/xhpast/rules/__tests__/formatted-string/formatted-string.lint-test
@@ -1,33 +1,44 @@
<?php
printf();
printf(null);
printf('');
sprintf('%s');
pht('%s', 'foo', 'bar');
fprintf(null, 'x');
queryfx(null, 'x', 'y');
foobar(null, null, '%s');
pht('x %s y');
pht('x %s y'.'z');
+
+pht(<<<HEREDOC
+a b c
+HEREDOC
+ );
+
+pht(<<<HEREDOC
+a %s c
+HEREDOC
+ );
~~~~~~~~~~
error:3:1:XHP54:Formatted String
error:7:1:XHP54:Formatted String
error:8:1:XHP54:Formatted String
error:11:1:XHP54:Formatted String
error:13:1:XHP54:Formatted String
error:15:1:XHP54:Formatted String
error:16:1:XHP54:Formatted String
+error:23:1:XHP54:Formatted String
~~~~~~~~~~
~~~~~~~~~~
{
"config": {
"xhpast.printf-functions": {
"foobar": 2
}
}
}
diff --git a/src/parser/xhpast/api/XHPASTNode.php b/src/parser/xhpast/api/XHPASTNode.php
index 4decaea1..0f685c5e 100644
--- a/src/parser/xhpast/api/XHPASTNode.php
+++ b/src/parser/xhpast/api/XHPASTNode.php
@@ -1,320 +1,342 @@
<?php
final class XHPASTNode extends AASTNode {
public function isStaticScalar() {
return in_array($this->getTypeName(), array(
'n_STRING_SCALAR',
'n_NUMERIC_SCALAR',
));
}
public function getDocblockToken() {
if ($this->l == -1) {
return null;
}
$tokens = $this->tree->getRawTokenStream();
for ($ii = $this->l - 1; $ii >= 0; $ii--) {
if ($tokens[$ii]->getTypeName() == 'T_DOC_COMMENT') {
return $tokens[$ii];
}
if (!$tokens[$ii]->isAnyWhitespace()) {
return null;
}
}
return null;
}
public function evalStatic() {
switch ($this->getTypeName()) {
case 'n_STATEMENT':
return $this->getChildByIndex(0)->evalStatic();
break;
case 'n_STRING_SCALAR':
- return (string)$this->getStringLiteralValue();
+ return phutil_string_cast($this->getStringLiteralValue());
+ case 'n_HEREDOC':
+ return phutil_string_cast($this->getStringLiteralValue());
case 'n_NUMERIC_SCALAR':
$value = $this->getSemanticString();
if (preg_match('/^0x/i', $value)) {
// Hex
$value = base_convert(substr($value, 2), 16, 10);
} else if (preg_match('/^0\d+$/i', $value)) {
// Octal
$value = base_convert(substr($value, 1), 8, 10);
}
return +$value;
case 'n_SYMBOL_NAME':
$value = $this->getSemanticString();
if ($value == 'INF') {
return INF;
}
switch (strtolower($value)) {
case 'true':
return true;
case 'false':
return false;
case 'null':
return null;
default:
throw new Exception(pht('Unrecognized symbol name.'));
}
break;
case 'n_UNARY_PREFIX_EXPRESSION':
$operator = $this->getChildOfType(0, 'n_OPERATOR');
$operand = $this->getChildByIndex(1);
switch ($operator->getSemanticString()) {
case '-':
return -$operand->evalStatic();
break;
case '+':
return $operand->evalStatic();
break;
default:
throw new Exception(
pht('Unexpected operator in static expression.'));
}
break;
case 'n_ARRAY_LITERAL':
$result = array();
$values = $this->getChildOfType(0, 'n_ARRAY_VALUE_LIST');
foreach ($values->getChildren() as $child) {
$key = $child->getChildByIndex(0);
$val = $child->getChildByIndex(1);
if ($key->getTypeName() == 'n_EMPTY') {
$result[] = $val->evalStatic();
} else {
$result[$key->evalStatic()] = $val->evalStatic();
}
}
return $result;
case 'n_CONCATENATION_LIST':
$result = '';
foreach ($this->getChildren() as $child) {
if ($child->getTypeName() == 'n_OPERATOR') {
continue;
}
$result .= $child->evalStatic();
}
return $result;
default:
throw new Exception(
pht(
'Unexpected node during static evaluation, of type: %s',
$this->getTypeName()));
}
}
public function isConstantString() {
return $this->checkIsConstantString();
}
public function isConstantStringWithMagicConstants() {
return $this->checkIsConstantString(array('n_MAGIC_SCALAR'));
}
private function checkIsConstantString(array $additional_types = array()) {
switch ($this->getTypeName()) {
case 'n_HEREDOC':
case 'n_STRING_SCALAR':
return !$this->getStringVariables();
case 'n_CONCATENATION_LIST':
foreach ($this->getChildren() as $child) {
if ($child->getTypeName() == 'n_OPERATOR') {
continue;
}
if (!$child->checkIsConstantString($additional_types)) {
return false;
}
}
return true;
default:
if (in_array($this->getTypeName(), $additional_types)) {
return true;
}
return false;
}
}
public function getStringVariables() {
$value = $this->getConcreteString();
switch ($this->getTypeName()) {
case 'n_HEREDOC':
if (preg_match("/^<<<\s*'/", $value)) { // Nowdoc: <<<'EOT'
return array();
}
break;
case 'n_STRING_SCALAR':
if ($value[0] == "'") {
return array();
}
break;
default:
throw new Exception(pht('Unexpected type %s.', $this->getTypeName()));
}
// We extract just the variable names and ignore properties and array keys.
$re = '/\\\\.|(\$|\{\$|\${)([a-z_\x7F-\xFF][a-z0-9_\x7F-\xFF]*)/i';
$matches = null;
preg_match_all($re, $value, $matches, PREG_OFFSET_CAPTURE);
// NOTE: The result format for this construction changed in PHP 7.4.
// See T13518.
$names = $matches[2];
foreach ($names as $name_idx => $name_match) {
if ($name_match === '') {
unset($names[$name_idx]);
continue;
}
if ($name_match[1] === -1) {
unset($names[$name_idx]);
continue;
}
}
$names = ipull($names, 0, 1);
return $names;
}
public function getStringLiteralValue() {
- if ($this->getTypeName() != 'n_STRING_SCALAR') {
- return null;
+ $type_name = $this->getTypeName();
+
+ if ($type_name === 'n_HEREDOC') {
+ $value = $this->getSemanticString();
+ $value = phutil_split_lines($value);
+ $value = array_slice($value, 1, -1);
+ $value = implode('', $value);
+
+ // Strip the final newline from value, this isn't part of the string
+ // literal.
+ $value = preg_replace('/(\r|\n|\r\n)\z/', '', $value);
+
+ return $this->newStringLiteralFromSemanticString($value);
}
- $value = $this->getSemanticString();
- $type = $value[0];
- $value = preg_replace('/^b?[\'"]|[\'"]$/i', '', $value);
- $esc = false;
- $len = strlen($value);
- $out = '';
-
- if ($type == "'") {
- // Single quoted strings treat everything as a literal except "\\" and
- // "\'".
- return str_replace(
- array('\\\\', '\\\''),
- array('\\', "'"),
- $value);
+ if ($type_name === 'n_STRING_SCALAR') {
+ $value = $this->getSemanticString();
+ $type = $value[0];
+ $value = preg_replace('/^b?[\'"]|[\'"]$/i', '', $value);
+
+ if ($type == "'") {
+ // Single quoted strings treat everything as a literal except "\\" and
+ // "\'".
+ return str_replace(
+ array('\\\\', '\\\''),
+ array('\\', "'"),
+ $value);
+ }
+
+ return $this->newStringLiteralFromSemanticString($value);
}
+ return null;
+ }
+
+ private function newStringLiteralFromSemanticString($value) {
// Double quoted strings treat "\X" as a literal if X isn't specifically
// a character which needs to be escaped -- e.g., "\q" and "\'" are
// literally "\q" and "\'". stripcslashes() is too aggressive, so find
// all these under-escaped backslashes and escape them.
+ $len = strlen($value);
+ $esc = false;
+ $out = '';
+
for ($ii = 0; $ii < $len; $ii++) {
$c = $value[$ii];
if ($esc) {
$esc = false;
switch ($c) {
case 'x':
$u = isset($value[$ii + 1]) ? $value[$ii + 1] : null;
if (!preg_match('/^[a-f0-9]/i', $u)) {
// PHP treats \x followed by anything which is not a hex digit
// as a literal \x.
$out .= '\\\\'.$c;
break;
}
/* fallthrough */
case 'n':
case 'r':
case 'f':
case 'v':
case '"':
case '$':
case 't':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
$out .= '\\'.$c;
break;
case 'e':
// Since PHP 5.4.0, this means "esc". However, stripcslashes() does
// not perform this conversion.
$out .= chr(27);
break;
default:
$out .= '\\\\'.$c;
break;
}
} else if ($c == '\\') {
$esc = true;
} else {
$out .= $c;
}
}
return stripcslashes($out);
}
/**
* Determines the parent namespace for a node.
*
* Traverses the AST upwards from a given node in order to determine the
* namespace in which the node is declared.
*
* To prevent any possible ambiguity, the returned namespace will always be
* prefixed with the namespace separator.
*
* @param XHPASTNode The input node.
* @return string|null The namespace which contains the input node, or
* `null` if no such node exists.
*/
public function getNamespace() {
$namespaces = $this
->getTree()
->getRootNode()
->selectDescendantsOfType('n_NAMESPACE')
->getRawNodes();
foreach (array_reverse($namespaces) as $namespace) {
if ($namespace->isAfter($this)) {
continue;
}
$body = $namespace->getChildByIndex(1);
if ($body->getTypeName() != 'n_EMPTY') {
if (!$body->containsDescendant($this)) {
continue;
}
}
return $namespace->getNamespaceName();
}
return null;
}
/**
* Returns the namespace name from a node of type `n_NAMESPACE`.
*
* @return string|null
*/
private function getNamespaceName() {
if ($this->getTypeName() != 'n_NAMESPACE') {
return null;
}
$namespace_name = $this->getChildByIndex(0);
if ($namespace_name->getTypeName() == 'n_EMPTY') {
return null;
}
return '\\'.$namespace_name->getConcreteString();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jan 23, 21:14 (1 d, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
601529
Default Alt Text
(11 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment