/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php

https://github.com/vimeo/psalm · PHP · 294 lines · 237 code · 50 blank · 7 comment · 19 complexity · 2399375379154f832a907edd7b511773 MD5 · raw file

  1. <?php
  2. namespace Psalm\Internal\Analyzer\Statements\Expression;
  3. use PhpParser;
  4. use Psalm\Internal\Analyzer\AlgebraAnalyzer;
  5. use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
  6. use \Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer;
  7. use Psalm\Internal\Analyzer\StatementsAnalyzer;
  8. use Psalm\CodeLocation;
  9. use Psalm\Context;
  10. use Psalm\Type;
  11. use Psalm\Type\Algebra;
  12. use Psalm\Type\Reconciler;
  13. use Psalm\Internal\Type\AssertionReconciler;
  14. use function array_merge;
  15. use function array_map;
  16. use function array_filter;
  17. use function array_values;
  18. use function array_keys;
  19. use function preg_match;
  20. use function preg_quote;
  21. use function array_intersect_key;
  22. /**
  23. * @internal
  24. */
  25. class TernaryAnalyzer
  26. {
  27. public static function analyze(
  28. StatementsAnalyzer $statements_analyzer,
  29. PhpParser\Node\Expr\Ternary $stmt,
  30. Context $context
  31. ) : bool {
  32. $codebase = $statements_analyzer->getCodebase();
  33. $if_scope = new \Psalm\Internal\Scope\IfScope();
  34. try {
  35. $if_conditional_scope = IfAnalyzer::analyzeIfConditional(
  36. $statements_analyzer,
  37. $stmt->cond,
  38. $context,
  39. $codebase,
  40. $if_scope,
  41. $context->branch_point ?: (int) $stmt->getAttribute('startFilePos')
  42. );
  43. $if_context = $if_conditional_scope->if_context;
  44. $cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
  45. $cond_assigned_var_ids = $if_conditional_scope->cond_assigned_var_ids;
  46. } catch (\Psalm\Exception\ScopeAnalysisException $e) {
  47. return false;
  48. }
  49. $codebase = $statements_analyzer->getCodebase();
  50. $cond_id = \spl_object_id($stmt->cond);
  51. $if_clauses = \Psalm\Type\Algebra::getFormula(
  52. $cond_id,
  53. $cond_id,
  54. $stmt->cond,
  55. $context->self,
  56. $statements_analyzer,
  57. $codebase
  58. );
  59. $mixed_var_ids = [];
  60. foreach ($context->vars_in_scope as $var_id => $type) {
  61. if ($type->hasMixed()) {
  62. $mixed_var_ids[] = $var_id;
  63. }
  64. }
  65. foreach ($context->vars_possibly_in_scope as $var_id => $_) {
  66. if (!isset($context->vars_in_scope[$var_id])) {
  67. $mixed_var_ids[] = $var_id;
  68. }
  69. }
  70. $if_clauses = array_values(
  71. array_map(
  72. /**
  73. * @return \Psalm\Internal\Clause
  74. */
  75. function (\Psalm\Internal\Clause $c) use ($mixed_var_ids, $cond_id): \Psalm\Internal\Clause {
  76. $keys = array_keys($c->possibilities);
  77. $mixed_var_ids = \array_diff($mixed_var_ids, $keys);
  78. foreach ($keys as $key) {
  79. foreach ($mixed_var_ids as $mixed_var_id) {
  80. if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
  81. return new \Psalm\Internal\Clause([], $cond_id, $cond_id, true);
  82. }
  83. }
  84. }
  85. return $c;
  86. },
  87. $if_clauses
  88. )
  89. );
  90. // this will see whether any of the clauses in set A conflict with the clauses in set B
  91. AlgebraAnalyzer::checkForParadox(
  92. $context->clauses,
  93. $if_clauses,
  94. $statements_analyzer,
  95. $stmt->cond,
  96. $cond_assigned_var_ids
  97. );
  98. $ternary_clauses = array_merge($context->clauses, $if_clauses);
  99. if ($if_context->reconciled_expression_clauses) {
  100. $reconciled_expression_clauses = $if_context->reconciled_expression_clauses;
  101. $ternary_clauses = array_values(
  102. array_filter(
  103. $ternary_clauses,
  104. function ($c) use ($reconciled_expression_clauses): bool {
  105. return !\in_array($c->hash, $reconciled_expression_clauses);
  106. }
  107. )
  108. );
  109. }
  110. $ternary_clauses = Algebra::simplifyCNF($ternary_clauses);
  111. $negated_clauses = Algebra::negateFormula($if_clauses);
  112. $negated_if_types = Algebra::getTruthsFromFormula(
  113. Algebra::simplifyCNF(
  114. array_merge($context->clauses, $negated_clauses)
  115. )
  116. );
  117. $active_if_types = [];
  118. $reconcilable_if_types = Algebra::getTruthsFromFormula(
  119. $ternary_clauses,
  120. $cond_id,
  121. $cond_referenced_var_ids,
  122. $active_if_types
  123. );
  124. $changed_var_ids = [];
  125. if ($reconcilable_if_types) {
  126. $if_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
  127. $reconcilable_if_types,
  128. $active_if_types,
  129. $if_context->vars_in_scope,
  130. $changed_var_ids,
  131. $cond_referenced_var_ids,
  132. $statements_analyzer,
  133. $statements_analyzer->getTemplateTypeMap() ?: [],
  134. $if_context->inside_loop,
  135. new CodeLocation($statements_analyzer->getSource(), $stmt->cond)
  136. );
  137. $if_context->vars_in_scope = $if_vars_in_scope_reconciled;
  138. }
  139. $t_else_context = clone $context;
  140. if ($stmt->if) {
  141. if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->if, $if_context) === false) {
  142. return false;
  143. }
  144. foreach ($if_context->vars_in_scope as $var_id => $type) {
  145. if (isset($context->vars_in_scope[$var_id])) {
  146. $context->vars_in_scope[$var_id] = Type::combineUnionTypes($context->vars_in_scope[$var_id], $type);
  147. }
  148. }
  149. $context->referenced_var_ids = array_merge(
  150. $context->referenced_var_ids,
  151. $if_context->referenced_var_ids
  152. );
  153. $context->unreferenced_vars = array_intersect_key(
  154. $context->unreferenced_vars,
  155. $if_context->unreferenced_vars
  156. );
  157. }
  158. $t_else_context->clauses = Algebra::simplifyCNF(
  159. array_merge(
  160. $t_else_context->clauses,
  161. $negated_clauses
  162. )
  163. );
  164. if ($negated_if_types) {
  165. $changed_var_ids = [];
  166. $t_else_vars_in_scope_reconciled = Reconciler::reconcileKeyedTypes(
  167. $negated_if_types,
  168. $negated_if_types,
  169. $t_else_context->vars_in_scope,
  170. $changed_var_ids,
  171. $cond_referenced_var_ids,
  172. $statements_analyzer,
  173. $statements_analyzer->getTemplateTypeMap() ?: [],
  174. $t_else_context->inside_loop,
  175. new CodeLocation($statements_analyzer->getSource(), $stmt->else)
  176. );
  177. $t_else_context->vars_in_scope = $t_else_vars_in_scope_reconciled;
  178. $t_else_context->clauses = Context::removeReconciledClauses($t_else_context->clauses, $changed_var_ids)[0];
  179. }
  180. if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->else, $t_else_context) === false) {
  181. return false;
  182. }
  183. foreach ($t_else_context->vars_in_scope as $var_id => $type) {
  184. if (isset($context->vars_in_scope[$var_id])) {
  185. $context->vars_in_scope[$var_id] = Type::combineUnionTypes(
  186. $context->vars_in_scope[$var_id],
  187. $type
  188. );
  189. } elseif (isset($if_context->vars_in_scope[$var_id])
  190. && isset($if_context->assigned_var_ids[$var_id])
  191. ) {
  192. $context->vars_in_scope[$var_id] = Type::combineUnionTypes(
  193. $if_context->vars_in_scope[$var_id],
  194. $type
  195. );
  196. }
  197. }
  198. $context->vars_possibly_in_scope = array_merge(
  199. $context->vars_possibly_in_scope,
  200. $if_context->vars_possibly_in_scope,
  201. $t_else_context->vars_possibly_in_scope
  202. );
  203. $context->referenced_var_ids = array_merge(
  204. $context->referenced_var_ids,
  205. $t_else_context->referenced_var_ids
  206. );
  207. $context->unreferenced_vars = array_intersect_key(
  208. $context->unreferenced_vars,
  209. $t_else_context->unreferenced_vars
  210. );
  211. foreach ($context->unreferenced_vars as $var_id => $_) {
  212. if (isset($t_else_context->unreferenced_vars[$var_id])) {
  213. $context->unreferenced_vars[$var_id] += $t_else_context->unreferenced_vars[$var_id];
  214. }
  215. if (isset($if_context->unreferenced_vars[$var_id])) {
  216. $context->unreferenced_vars[$var_id] += $if_context->unreferenced_vars[$var_id];
  217. }
  218. }
  219. $lhs_type = null;
  220. if ($stmt->if) {
  221. if ($stmt_if_type = $statements_analyzer->node_data->getType($stmt->if)) {
  222. $lhs_type = $stmt_if_type;
  223. }
  224. } elseif ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) {
  225. $if_return_type_reconciled = AssertionReconciler::reconcile(
  226. '!falsy',
  227. clone $stmt_cond_type,
  228. '',
  229. $statements_analyzer,
  230. $context->inside_loop,
  231. [],
  232. new CodeLocation($statements_analyzer->getSource(), $stmt),
  233. $statements_analyzer->getSuppressedIssues()
  234. );
  235. $lhs_type = $if_return_type_reconciled;
  236. }
  237. if ($lhs_type && ($stmt_else_type = $statements_analyzer->node_data->getType($stmt->else))) {
  238. $statements_analyzer->node_data->setType($stmt, Type::combineUnionTypes($lhs_type, $stmt_else_type));
  239. } else {
  240. $statements_analyzer->node_data->setType($stmt, Type::getMixed());
  241. }
  242. return true;
  243. }
  244. }