3
final class ArcanistPhutilXHPASTLinter extends ArcanistBaseXHPASTLinter {
5
const LINT_ARRAY_COMBINE = 2;
6
const LINT_DEPRECATED_FUNCTION = 3;
7
const LINT_UNSAFE_DYNAMIC_STRING = 4;
8
const LINT_RAGGED_CLASSTREE_EDGE = 5;
10
private $deprecatedFunctions = array();
11
private $dynamicStringFunctions = array();
12
private $dynamicStringClasses = array();
14
public function getInfoName() {
15
return 'XHPAST/libphutil Lint';
18
public function getInfoDescription() {
20
'Use XHPAST to run libphutil-specific rules on a PHP library. This '.
21
'linter is intended for use in Phabricator libraries and extensions.');
24
public function setDeprecatedFunctions(array $map) {
25
$this->deprecatedFunctions = $map;
29
public function setDynamicStringFunctions(array $map) {
30
$this->dynamicStringFunctions = $map;
34
public function setDynamicStringClasses(array $map) {
35
$this->dynamicStringClasses = $map;
39
public function getLintNameMap() {
41
self::LINT_ARRAY_COMBINE => pht(
44
self::LINT_DEPRECATED_FUNCTION => pht(
45
'Use of Deprecated Function'),
46
self::LINT_UNSAFE_DYNAMIC_STRING => pht(
47
'Unsafe Usage of Dynamic String'),
48
self::LINT_RAGGED_CLASSTREE_EDGE => pht(
55
public function getLintSeverityMap() {
56
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
58
self::LINT_ARRAY_COMBINE => $warning,
59
self::LINT_DEPRECATED_FUNCTION => $warning,
60
self::LINT_UNSAFE_DYNAMIC_STRING => $warning,
61
self::LINT_RAGGED_CLASSTREE_EDGE => $warning,
65
public function getLinterName() {
69
public function getLinterConfigurationName() {
70
return 'phutil-xhpast';
73
public function getVersion() {
74
// The version number should be incremented whenever a new rule is added.
78
public function getLinterConfigurationOptions() {
80
'phutil-xhpast.deprecated.functions' => array(
81
'type' => 'optional map<string, string>',
83
'Functions which should should be considered deprecated.'),
85
'phutil-xhpast.dynamic-string.functions' => array(
86
'type' => 'optional map<string, string>',
88
'Functions which should should not be used because they represent '.
89
'the unsafe usage of dynamic strings.'),
91
'phutil-xhpast.dynamic-string.classes' => array(
92
'type' => 'optional map<string, string>',
94
'Classes which should should not be used because they represent the '.
95
'unsafe usage of dynamic strings.'),
99
return $options + parent::getLinterConfigurationOptions();
102
public function setLinterConfigurationValue($key, $value) {
104
case 'phutil-xhpast.deprecated.functions':
105
$this->setDeprecatedFunctions($value);
107
case 'phutil-xhpast.dynamic-string.functions':
108
$this->setDynamicStringFunctions($value);
110
case 'phutil-xhpast.dynamic-string.classes':
111
$this->setDynamicStringClasses($value);
115
return parent::setLinterConfigurationValue($key, $value);
118
protected function resolveFuture($path, Future $future) {
119
$tree = $this->getXHPASTLinter()->getXHPASTTreeForPath($path);
124
$root = $tree->getRootNode();
126
$method_codes = array(
127
'lintArrayCombine' => self::LINT_ARRAY_COMBINE,
128
'lintUnsafeDynamicString' => self::LINT_UNSAFE_DYNAMIC_STRING,
129
'lintDeprecatedFunctions' => self::LINT_DEPRECATED_FUNCTION,
130
'lintRaggedClasstreeEdges' => self::LINT_RAGGED_CLASSTREE_EDGE,
133
foreach ($method_codes as $method => $codes) {
134
foreach ((array)$codes as $code) {
135
if ($this->isCodeEnabled($code)) {
136
call_user_func(array($this, $method), $root);
143
private function lintUnsafeDynamicString(XHPASTNode $root) {
144
$safe = $this->dynamicStringFunctions + array(
156
'phutil_passthru' => 0,
167
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
168
$this->lintUnsafeDynamicStringCall($calls, $safe);
170
$safe = $this->dynamicStringClasses + array(
174
$news = $root->selectDescendantsOfType('n_NEW');
175
$this->lintUnsafeDynamicStringCall($news, $safe);
178
private function lintUnsafeDynamicStringCall(
182
$safe = array_combine(
183
array_map('strtolower', array_keys($safe)),
186
foreach ($calls as $call) {
187
$name = $call->getChildByIndex(0)->getConcreteString();
188
$param = idx($safe, strtolower($name));
190
if ($param === null) {
194
$parameters = $call->getChildByIndex(1);
195
if (count($parameters->getChildren()) <= $param) {
199
$identifier = $parameters->getChildByIndex($param);
200
if (!$identifier->isConstantString()) {
201
$this->raiseLintAtNode(
203
self::LINT_UNSAFE_DYNAMIC_STRING,
205
"Parameter %d of %s should be a scalar string, ".
206
"otherwise it's not safe.",
213
private function lintArrayCombine(XHPASTNode $root) {
214
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
215
foreach ($function_calls as $call) {
216
$name = $call->getChildByIndex(0)->getConcreteString();
217
if (strcasecmp($name, 'array_combine') == 0) {
218
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
219
if (count($parameter_list->getChildren()) !== 2) {
220
// Wrong number of parameters, but raise that elsewhere if we want.
224
$first = $parameter_list->getChildByIndex(0);
225
$second = $parameter_list->getChildByIndex(1);
227
if ($first->getConcreteString() == $second->getConcreteString()) {
228
$this->raiseLintAtNode(
230
self::LINT_ARRAY_COMBINE,
232
'Prior to PHP 5.4, `%s` fails when given empty arrays. '.
233
'Prefer to write `%s` as `%s`.',
235
'array_combine(x, x)',
242
private function lintDeprecatedFunctions(XHPASTNode $root) {
243
$map = $this->deprecatedFunctions;
245
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
246
foreach ($function_calls as $call) {
247
$name = $call->getChildByIndex(0)->getConcreteString();
249
$name = strtolower($name);
250
if (empty($map[$name])) {
254
$this->raiseLintAtNode(
256
self::LINT_DEPRECATED_FUNCTION,
261
private function lintRaggedClasstreeEdges(XHPASTNode $root) {
262
$parser = new PhutilDocblockParser();
264
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
265
foreach ($classes as $class) {
267
$is_abstract = false;
268
$is_concrete_extensible = false;
270
$attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES');
271
foreach ($attributes->getChildren() as $child) {
272
if ($child->getConcreteString() == 'final') {
275
if ($child->getConcreteString() == 'abstract') {
280
$docblock = $class->getDocblockToken();
282
list($text, $specials) = $parser->parse($docblock->getValue());
283
$is_concrete_extensible = idx($specials, 'concrete-extensible');
286
if (!$is_final && !$is_abstract && !$is_concrete_extensible) {
287
$this->raiseLintAtNode(
288
$class->getChildOfType(1, 'n_CLASS_NAME'),
289
self::LINT_RAGGED_CLASSTREE_EDGE,
291
"This class is neither '%s' nor '%s', and does not have ".
292
"a docblock marking it '%s'.",
295
'@concrete-extensible'));