PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/phplinter/Linter.php

http://github.com/robotis/PHPLinter
PHP | 550 lines | 381 code | 6 blank | 163 comment | 64 complexity | 60b91e13a6e448c2c7082fdd842b7d1d MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. ----------------------------------------------------------------------+
  4. * @desc PHPLinter
  5. ----------------------------------------------------------------------+
  6. * @file Linter.php
  7. * @author Jóhann T. Maríusson <jtm@robot.is>
  8. * @copyright
  9. * phplinter is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. ----------------------------------------------------------------------+
  23. */
  24. namespace phplinter;
  25. require_once dirname(__FILE__) . '/constants.php';
  26. /**
  27. ----------------------------------------------------------------------+
  28. * @desc Linter. Measures code and splits into nodes.
  29. ----------------------------------------------------------------------+
  30. */
  31. class Linter {
  32. /* @var float */
  33. protected $score;
  34. /* @var Array */
  35. protected $tokens;
  36. /**
  37. ----------------------------------------------------------------------+
  38. * @desc Create new linter instance
  39. * @param String Filename
  40. * @param object Config object
  41. ----------------------------------------------------------------------+
  42. */
  43. public function __construct($file, Config $config) {
  44. $this->config = $config;
  45. $this->file = $file;
  46. exec('php -l ' . escapeshellarg($file), $error, $code);
  47. if($code === 0) {
  48. $this->tokens = Tokenizer::tokenize($file);
  49. $this->tcount = count($this->tokens);
  50. $this->score = 0;
  51. $this->ignore_next = array();
  52. $this->scope = array();
  53. $this->globals = require dirname(__FILE__) . '/globals.php';
  54. } else {
  55. $this->score = false;
  56. }
  57. $this->report = array();
  58. }
  59. /**
  60. ----------------------------------------------------------------------+
  61. * @desc Internal profiling
  62. * @param Bool
  63. ----------------------------------------------------------------------+
  64. */
  65. protected function profile($flushmsg=false) {
  66. if(defined('PHPL_PROFILE_ON')) {
  67. $now = microtime(true);
  68. if($flushmsg) {
  69. $time = $this->ptime - $now;
  70. echo "$time -> $flushmsg\n";
  71. } else {
  72. $this->ptime = $now;
  73. }
  74. }
  75. }
  76. /**
  77. ----------------------------------------------------------------------+
  78. * @desc Lint current file
  79. * @return Array
  80. ----------------------------------------------------------------------+
  81. */
  82. public function lint() {
  83. $this->node = null;
  84. if(is_null($this->tokens)) {
  85. $this->debug("Syntax error in file.. Skipping\n", 0, OPT_VERBOSE);
  86. } elseif($this->tcount === 0) {
  87. $this->debug("Empty file.. Skipping\n", 0, OPT_VERBOSE);
  88. } else {
  89. $this->node = $this->measure_file();
  90. $lint = new Lint\LFile($this->node, $this->config);
  91. $this->report = $lint->lint();
  92. $this->score = $lint->penalty();
  93. if(!empty($this->report)) {
  94. foreach($this->report as $_) $arr[] = $_['line'];
  95. array_multisort($arr, SORT_ASC, $this->report);
  96. }
  97. }
  98. return $this->report;
  99. }
  100. /**
  101. ----------------------------------------------------------------------+
  102. * @desc Return nodes
  103. * @return Array
  104. ----------------------------------------------------------------------+
  105. */
  106. public function nodes() {
  107. if($this->node) {
  108. $this->node->clean();
  109. return $this->node;
  110. }
  111. return null;
  112. }
  113. /**
  114. ----------------------------------------------------------------------+
  115. * @desc Measure file scope
  116. ----------------------------------------------------------------------+
  117. */
  118. protected function measure_file() {
  119. $this->profile();
  120. $node = new Lint\Node();
  121. $node->type = T_FILE;
  122. $node->file = $this->file;
  123. $node->parent = $this->file;
  124. $parts = explode('/', $this->file);
  125. $node->name = array_pop($parts);
  126. $node->owner = $this->file;
  127. $node->tokens = array();
  128. $node->depth = 0;
  129. $node = $this->measure(0, $node, $i);
  130. $node->end_line = $this->tokens[$i][2];
  131. $node->length = ($node->end_line - $node->start_line);
  132. $node->token_count = count($node->tokens);
  133. $this->profile('measure_file::' . $this->file);
  134. return $node;
  135. }
  136. /**
  137. ----------------------------------------------------------------------+
  138. * @desc Split token stream into nodes of type function, comment,
  139. * class or method.
  140. * @param int Start position
  141. * @param Object Node
  142. * @param int Depth
  143. * @return int
  144. ----------------------------------------------------------------------+
  145. */
  146. protected function measure($pos, Lint\Node $node, &$ret) {
  147. $start = $this->last_newline($pos);
  148. $node->start = $start;
  149. if($node->type === T_FILE) {
  150. $node->start_line = 1;
  151. $node->empty = false;
  152. } else {
  153. $node->start_line = $this->tokens[$start][2] + 1;
  154. $node->empty = true;
  155. }
  156. $this->debug(sprintf('In Node `%s` of type `%s` line %d; Owned by `%s`'
  157. ,$node->name
  158. ,Tokenizer::token_name($node->type)
  159. ,$node->start_line
  160. ,$node->owner
  161. ), $node->depth, OPT_SCOPE_MAP, true);
  162. $this->scope = array();
  163. $next_node = new Lint\Node();
  164. $last_comment = null;
  165. $tokens = $this->tokens;
  166. for($i = $pos; $i < $this->tcount; $i++) {
  167. if(count($this->scope) > 0
  168. && $node->empty
  169. && Tokenizer::meaningfull($tokens[$i][0])
  170. && $tokens[$i][0] !== T_CURLY_CLOSE)
  171. {
  172. $node->empty = false;
  173. }
  174. if(!empty($this->ignore_next)
  175. && $tokens[$i][0] === $this->ignore_next[count($this->ignore_next) - 1])
  176. {
  177. $node->tokens[] = $tokens[$i];
  178. array_pop($this->ignore_next);
  179. continue;
  180. }
  181. switch($tokens[$i][0]) {
  182. case T_INLINE_HTML:
  183. // Gather inline html into one token
  184. $node->tokens[] = $tokens[$i];
  185. while(++$i < $this->tcount
  186. && in_array($tokens[$i][0], array(T_INLINE_HTML, T_NEWLINE)));
  187. $i--;
  188. break;
  189. case T_ELSE:
  190. // treat `else if` like `elseif`
  191. // perhaps the tokenizer should do this ?
  192. if($tokens[$this->next($i)][0] === T_IF) {
  193. $this->tokens[$i][0] = T_ELSEIF;
  194. $this->tokens[$i][1] = 'elseif';
  195. $this->ignore_next[] = T_IF;
  196. }
  197. $this->open_scope($i, $node);
  198. break;
  199. case T_IF:
  200. case T_ELSEIF:
  201. case T_THEN:
  202. case T_FOREACH:
  203. case T_WHILE:
  204. case T_SWITCH:
  205. $this->open_scope($i, $node);
  206. break;
  207. case T_FOR:
  208. $this->open_scope($i, $node);
  209. $this->ignore_next[] = T_SEMICOLON;
  210. $this->ignore_next[] = T_SEMICOLON;
  211. break;
  212. case T_BASIC_CURLY_OPEN:
  213. $this->open_scope($i, $node);
  214. break;
  215. case T_SEMICOLON:
  216. $this->close_scope($i, $node);
  217. $node->tokens[] = $tokens[$i];
  218. if(empty($this->scope) && $node->empty) {
  219. $i++;
  220. break 2;
  221. }
  222. break;
  223. case T_CURLY_CLOSE:
  224. $this->close_scope($i, $node);
  225. if(empty($this->scope) && $node->type !== T_FILE) {
  226. $i++;
  227. break 2;
  228. }
  229. break;
  230. case T_CURLY_OPEN:
  231. $this->ignore_next[] = T_CURLY_CLOSE;
  232. break;
  233. case T_PUBLIC:
  234. case T_PRIVATE:
  235. case T_PROTECTED:
  236. $next_node->visibility = Tokenizer::token_name($tokens[$i][0]);
  237. break;
  238. case T_ABSTRACT:
  239. $next_node->abstract = true;
  240. break;
  241. case T_STATIC:
  242. $next_node->static = true;
  243. break;
  244. case T_COMMENT:
  245. $node->tokens[] = $tokens[$i];
  246. $node->comments[] = $this->measure_comment($i, $node->depth, $i);
  247. break;
  248. case T_DOC_COMMENT:
  249. $node->tokens[] = $tokens[$i];
  250. if($last_comment) {
  251. $node->comments[] = $last_comment;
  252. }
  253. $last_comment = $this->measure_comment($i, $node->depth, $i);
  254. break;
  255. case T_CLASS:
  256. case T_INTERFACE:
  257. case T_FUNCTION:
  258. list($type, $name, $owner) = $this->determine_type($i, $node, $next_node);
  259. $next_node->type = $type;
  260. $next_node->name = $name;
  261. $next_node->depth = $node->depth + 1;
  262. $next_node->owner = $owner;
  263. $next_node->file = $node->file;
  264. if($last_comment) {
  265. $next_node->comments[] = $last_comment;
  266. }
  267. $node->tokens[] = $tokens[$i];
  268. // preserve scope
  269. $scope = $this->scope;
  270. $node->nodes[] = $this->measure($i+1, $next_node, $i);
  271. $this->scope = $scope;
  272. $next_node = new Lint\Node();
  273. $last_comment = null;
  274. break;
  275. default:
  276. $node->tokens[] = $tokens[$i];
  277. break;
  278. }
  279. }
  280. // In case $i is over the buffer
  281. $node->end = ($i >= $this->tcount)
  282. ? --$i : $i;
  283. $node->end_line = $tokens[$i][2];
  284. $node->length = ($node->end_line - $node->start_line);
  285. $node->token_count = count($node->tokens);
  286. $ret = ($i > 0) ? --$i : $i;
  287. $this->debug(sprintf('Exiting Node `%s` of type `%s` line %d'
  288. ,$node->name
  289. ,Tokenizer::token_name($node->type)
  290. ,$node->end_line
  291. ), $node->depth, OPT_SCOPE_MAP, true);
  292. return $node;
  293. }
  294. /**
  295. ----------------------------------------------------------------------+
  296. * @desc Determine type of new node
  297. * @param int Position
  298. * @param Object Current Node
  299. * @param Object Next Node
  300. * @return Array
  301. ----------------------------------------------------------------------+
  302. */
  303. protected function determine_type($pos, Lint\Node $node, Lint\Node &$next_node) {
  304. $tokens = $this->tokens;
  305. $next = $this->find($pos, array(T_STRING, T_PARENTHESIS_OPEN));
  306. $type = $tokens[$pos][0];
  307. if($next === false || $tokens[$next][0] === T_PARENTHESIS_OPEN) {
  308. // anonymous functions
  309. $name = 'anonymous';
  310. $type = T_ANON_FUNCTION;
  311. } else {
  312. $name = $tokens[$next][1];
  313. if(in_array($node->type, array(T_CLASS, T_INTERFACE))
  314. && $tokens[$pos][0] == T_FUNCTION)
  315. {
  316. $type = T_METHOD;
  317. if($node->type === T_INTERFACE) {
  318. $next_node->abstract = true;
  319. }
  320. }
  321. }
  322. if($type === T_METHOD || $type === T_ANON_FUNCTION) {
  323. $owner = $node->name;
  324. } else $owner = $node->owner;
  325. return array($type, $name, $owner);
  326. }
  327. /**
  328. ----------------------------------------------------------------------+
  329. * @desc Open scope, set scope token
  330. ----------------------------------------------------------------------+
  331. */
  332. protected function open_scope($pos, Lint\Node $node) {
  333. $token = $this->tokens[$pos];
  334. $scope = true;
  335. if($token[0] === T_BASIC_CURLY_OPEN && !empty($this->scope)) {
  336. // Scopes are closed by `;` if not opened by `{`.
  337. $last = array_pop($this->scope);
  338. if($last[0] !== T_BASIC_CURLY_OPEN) {
  339. $last[0] = T_BASIC_CURLY_OPEN;
  340. $scope = false;
  341. }
  342. $this->scope[] = $last;
  343. }
  344. if($scope) {
  345. $depth = count($this->scope) + $node->depth;
  346. $this->debug(sprintf('Scope opened by `%s` line %d'
  347. ,$token[1]
  348. ,$token[2])
  349. ,$depth, OPT_SCOPE_MAP, true);
  350. $this->scope[] = $token;
  351. $node->tokens[] = array(T_OPEN_SCOPE, $token[1], $token[2]);
  352. }
  353. }
  354. /**
  355. ----------------------------------------------------------------------+
  356. * @desc Close scope, set scope token
  357. ----------------------------------------------------------------------+
  358. */
  359. protected function close_scope($pos, Lint\Node $node) {
  360. $token = $this->tokens[$pos];
  361. if($token[0] === T_SEMICOLON) {
  362. if(!empty($this->scope)) {
  363. // Nested scopes if not opened by `{` are
  364. // terminated all at once.
  365. while($last = array_pop($this->scope)) {
  366. if($last[0] === T_BASIC_CURLY_OPEN) {
  367. $this->scope[] = $last;
  368. break;
  369. }
  370. $depth = count($this->scope) + $node->depth;
  371. $this->debug(sprintf('Scope closed by `%s` line %d'
  372. ,$token[1]
  373. ,$token[2])
  374. ,$depth, OPT_SCOPE_MAP, true);
  375. $node->tokens[] = array(T_CLOSE_SCOPE, $token[1], $token[2]);
  376. }
  377. }
  378. } else {
  379. $depth = count($this->scope) + $node->depth;
  380. $this->debug(sprintf('Scope closed by `%s` line %d'
  381. ,$token[1]
  382. ,$token[2])
  383. ,$depth, OPT_SCOPE_MAP, true);
  384. array_pop($this->scope);
  385. $node->tokens[] = array(T_CLOSE_SCOPE, $token[1], $token[2]);
  386. }
  387. }
  388. /**
  389. ----------------------------------------------------------------------+
  390. * @desc Output debug info
  391. * @param $out String
  392. * @param $depth int
  393. ----------------------------------------------------------------------+
  394. */
  395. protected function debug($out, $depth=0, $mode=OPT_DEBUG, $smap=false) {
  396. if($this->config->check($mode)) {
  397. if($smap) {
  398. $tabs = str_pad('', $depth*2, "|\t");
  399. } else {
  400. $tabs = str_pad('', $depth, "\t");
  401. }
  402. echo "{$tabs}$out\n";
  403. }
  404. }
  405. /**
  406. ----------------------------------------------------------------------+
  407. * @desc Measure comment
  408. * @param $pos int
  409. * @param $depth int
  410. * @return int
  411. ----------------------------------------------------------------------+
  412. */
  413. protected function measure_comment($pos, $depth, &$ret) {
  414. $node = new Lint\Node();
  415. $node->start = $pos;
  416. $node->start_line = $this->tokens[$pos][2];
  417. $node->type = $this->tokens[$pos][0];
  418. $node->name = 'comment';
  419. $depth += count($this->scope);
  420. $this->debug("In comment at {$node->start_line}", $depth, OPT_SCOPE_MAP, true);
  421. $t = $this->tokens;
  422. for($i = $pos;$i < $this->tcount;$i++) {
  423. if(Tokenizer::meaningfull($t[$i][0])) {
  424. $i--;
  425. break;
  426. }
  427. $node->tokens[] = $t[$i];
  428. if(preg_match('/\*\//u', $t[$i][1])) {
  429. // End of comment.
  430. break;
  431. }
  432. }
  433. if($i === $this->tcount) $i--;
  434. $node->end = $i;
  435. $node->end_line = $t[$i][2];
  436. $this->debug("Exiting comment at {$node->end_line}", $depth, OPT_SCOPE_MAP, true);
  437. $ret = $i;
  438. return $node;
  439. }
  440. /**
  441. ----------------------------------------------------------------------+
  442. * @desc Find the next token.
  443. * @param int Start
  444. * @param mixed tokens to search for
  445. * @param int search limit
  446. * @return Int
  447. ----------------------------------------------------------------------+
  448. */
  449. protected function find($pos, $token, $limit=10) {
  450. $i = $pos;
  451. if(!is_array($token)) $token = array($token);
  452. while(true) {
  453. if(!isset($this->tokens[$i+1])) {
  454. return false;
  455. }
  456. if(in_array($this->tokens[++$i][0], $token)) {
  457. return $i;
  458. }
  459. if(!empty($limit) && ($i - $pos) == $limit)
  460. return false;
  461. }
  462. }
  463. /**
  464. ----------------------------------------------------------------------+
  465. * @desc Return the next meaningfull token
  466. * @param int
  467. * @return Int
  468. ----------------------------------------------------------------------+
  469. */
  470. protected function next($pos) {
  471. $i = $pos;
  472. while(true) {
  473. if(!isset($this->tokens[$i+1]))
  474. return false;
  475. if(Tokenizer::meaningfull($this->tokens[++$i][0]))
  476. return $i;
  477. }
  478. }
  479. /**
  480. ----------------------------------------------------------------------+
  481. * @desc Gather all token until not in $tokens
  482. * @param int
  483. * @param Array
  484. * @return Int
  485. ----------------------------------------------------------------------+
  486. */
  487. protected function gather(&$pos, $tokens) {
  488. while(++$pos < $this->tcount) {
  489. if(!in_array($this->tokens[$pos][0], $tokens)) {
  490. break;
  491. }
  492. $tokens[] = $this->tokens[$pos];
  493. }
  494. return --$pos;
  495. }
  496. /**
  497. ----------------------------------------------------------------------+
  498. * @desc Return location of previous meaningfull token
  499. * @param int
  500. * @return Int
  501. ----------------------------------------------------------------------+
  502. */
  503. protected function prev($pos) {
  504. $i = $pos;
  505. while($i >= 0) {
  506. if(Tokenizer::meaningfull($this->tokens[--$i][0]))
  507. return $i;
  508. }
  509. }
  510. /**
  511. ----------------------------------------------------------------------+
  512. * @desc Report penalty
  513. * @return float
  514. ----------------------------------------------------------------------+
  515. */
  516. public function penalty() {
  517. return empty($this->score) ? 0 : $this->score;
  518. }
  519. /**
  520. ----------------------------------------------------------------------+
  521. * @desc Report penalty
  522. * @return float
  523. ----------------------------------------------------------------------+
  524. */
  525. public function score() {
  526. if($this->score === false) return 0;
  527. return round(floatval(SCORE_FULL + $this->score), 2);
  528. }
  529. /**
  530. ----------------------------------------------------------------------+
  531. * @desc Find the last newline token.
  532. * @param $pos Int
  533. * @return Int
  534. ----------------------------------------------------------------------+
  535. */
  536. protected function last_newline($pos) {
  537. $i = $pos;
  538. while($i > 0) {
  539. if($this->tokens[--$i][0] == T_NEWLINE)
  540. break;
  541. }
  542. return $i;
  543. }
  544. }