11package fr .adrienbrault .idea .symfony2plugin .intentions .php ;
22
33import com .intellij .codeInsight .intention .PsiElementBaseIntentionAction ;
4+ import com .intellij .codeInsight .intention .preview .IntentionPreviewInfo ;
45import com .intellij .lang .LanguageImportStatements ;
56import com .intellij .openapi .editor .Document ;
67import com .intellij .openapi .editor .Editor ;
1213import com .intellij .psi .codeStyle .CodeStyleManager ;
1314import com .intellij .psi .util .PsiTreeUtil ;
1415import com .intellij .util .IncorrectOperationException ;
16+ import com .jetbrains .php .lang .psi .PhpPsiElementFactory ;
1517import com .jetbrains .php .lang .psi .elements .*;
1618import fr .adrienbrault .idea .symfony2plugin .Symfony2ProjectComponent ;
1719import fr .adrienbrault .idea .symfony2plugin .util .CodeUtil ;
3335 * @author Daniel Espendiller <daniel@espendiller.net>
3436 */
3537public class CommandToInvokableIntention extends PsiElementBaseIntentionAction implements Iconable {
38+ @ Override
39+ public @ NotNull IntentionPreviewInfo generatePreview (@ NotNull Project project , @ NotNull Editor editor , @ NotNull PsiFile file ) {
40+ return IntentionPreviewInfo .EMPTY ;
41+ }
3642
3743 @ Override
3844 public void invoke (@ NotNull Project project , Editor editor , @ NotNull PsiElement psiElement ) throws IncorrectOperationException {
@@ -48,9 +54,12 @@ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement
4854
4955 // Remove "extends Command" FIRST, before other PSI modifications
5056 // This must be done before replacing methods to avoid PSI invalidation issues
51- removeCommandExtends (phpClass );
57+ if (removeCommandExtends (phpClass )) {
58+ // Remove parent::__construct() calls since we no longer extend Command
59+ removeParentConstructorCalls (phpClass );
60+ }
5261
53- // Get parameter names from execute method
62+ // Get parameter names from the execute method
5463 ParameterNames paramNames = extractParameterNames (executeMethod );
5564
5665 // Collect arguments and options from configure() method
@@ -69,14 +78,59 @@ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement
6978 optimizeImports (phpClass .getContainingFile ());
7079 }
7180
72- private void removeCommandExtends (@ NotNull PhpClass phpClass ) {
81+ private boolean removeCommandExtends (@ NotNull PhpClass phpClass ) {
7382 // Check if class extends Command
7483 if (!PhpElementsUtil .isInstanceOf (phpClass , "\\ Symfony\\ Component\\ Console\\ Command\\ Command" )) {
75- return ;
84+ return false ;
7685 }
7786
7887 // Use shared utility method to remove the extends clause
79- CodeUtil .removeExtendsClause (phpClass , "\\ Symfony\\ Component\\ Console\\ Command\\ Command" );
88+ return CodeUtil .removeExtendsClause (phpClass , "\\ Symfony\\ Component\\ Console\\ Command\\ Command" );
89+ }
90+
91+ /**
92+ * Removes parent::__construct() calls from the class constructor.
93+ * Since we're removing "extends Command", any parent constructor calls become invalid.
94+ */
95+ private void removeParentConstructorCalls (@ NotNull PhpClass phpClass ) {
96+ // Find the constructor method
97+ Method constructor = phpClass .getConstructor ();
98+ if (constructor == null ) {
99+ return ;
100+ }
101+
102+ // Find the method body
103+ GroupStatement body = PsiTreeUtil .findChildOfType (constructor , GroupStatement .class );
104+ if (body == null ) {
105+ return ;
106+ }
107+
108+ // Find all parent::__construct() calls
109+ Collection <MethodReference > methodReferences = PsiTreeUtil .findChildrenOfType (body , MethodReference .class );
110+ List <MethodReference > parentConstructorCalls = new ArrayList <>();
111+
112+ for (MethodReference methodRef : methodReferences ) {
113+ // Check if this is a parent::__construct() call
114+ if ("__construct" .equals (methodRef .getName ())) {
115+ PsiElement classReference = methodRef .getClassReference ();
116+ if (classReference != null && "parent" .equals (classReference .getText ())) {
117+ parentConstructorCalls .add (methodRef );
118+ }
119+ }
120+ }
121+
122+ if (parentConstructorCalls .isEmpty ()) {
123+ return ;
124+ }
125+
126+ // Remove each parent::__construct() call statement
127+ for (MethodReference parentCall : parentConstructorCalls ) {
128+ // Navigate up to find the statement containing this call
129+ PsiElement statement = PsiTreeUtil .getParentOfType (parentCall , com .jetbrains .php .lang .psi .elements .Statement .class );
130+ if (statement != null ) {
131+ statement .delete ();
132+ }
133+ }
80134 }
81135
82136 private void optimizeImports (@ NotNull PsiFile file ) {
@@ -336,6 +390,13 @@ private void migrateExecuteToInvoke(@NotNull Project project, @NotNull Method ex
336390 if (freshPhpClass != null ) {
337391 Method invokeMethod = freshPhpClass .findOwnMethodByName ("__invoke" );
338392 if (invokeMethod != null ) {
393+ // Replace $input->getArgument() and $input->getOption() calls with direct variables
394+ replaceInputMethodCallsWithVariables (invokeMethod , configureData , paramNames );
395+
396+ // Commit after replacements
397+ psiDocManager .commitDocument (document );
398+ psiDocManager .doPostponedOperationsAndUnblockDocument (document );
399+
339400 // Reformat the __invoke method
340401 reformatMethod (project , invokeMethod );
341402 }
@@ -346,6 +407,151 @@ private void migrateExecuteToInvoke(@NotNull Project project, @NotNull Method ex
346407 psiDocManager .doPostponedOperationsAndUnblockDocument (document );
347408 }
348409
410+ /**
411+ * Replaces $input->getArgument('name') and $input->getOption('name') calls with direct variable access.
412+ * Also handles type casts like (int) $input->getArgument('name').
413+ * Removes redundant self-assignments like $var = $var.
414+ */
415+ private void replaceInputMethodCallsWithVariables (@ NotNull Method invokeMethod , @ NotNull ConfigureData configureData , @ NotNull ParameterNames paramNames ) {
416+ GroupStatement body = PsiTreeUtil .findChildOfType (invokeMethod , GroupStatement .class );
417+ if (body == null ) {
418+ return ;
419+ }
420+
421+ // Build a map of argument/option names to their variable names
422+ java .util .Map <String , String > argumentVariableMap = new java .util .HashMap <>();
423+ java .util .Map <String , String > optionVariableMap = new java .util .HashMap <>();
424+
425+ for (ArgumentInfo arg : configureData .arguments ) {
426+ String variableName = convertToValidPhpVariableName (arg .name );
427+ argumentVariableMap .put (arg .name , variableName );
428+ }
429+
430+ for (OptionInfo opt : configureData .options ) {
431+ String variableName = convertToValidPhpVariableName (opt .name );
432+ optionVariableMap .put (opt .name , variableName );
433+ }
434+
435+ // Find all method calls in the body
436+ Collection <MethodReference > methodCalls = PsiTreeUtil .findChildrenOfType (body , MethodReference .class );
437+ List <MethodReference > callsToReplace = new ArrayList <>();
438+
439+ for (MethodReference methodCall : methodCalls ) {
440+ String methodName = methodCall .getName ();
441+ if (methodName == null ) {
442+ continue ;
443+ }
444+
445+ // Check if this is $input->getArgument() or $input->getOption() on InputInterface
446+ if ("getArgument" .equals (methodName )) {
447+ if (PhpElementsUtil .isMethodReferenceInstanceOf (methodCall , "\\ Symfony\\ Component\\ Console\\ Input\\ InputInterface" , "getArgument" )) {
448+ callsToReplace .add (methodCall );
449+ }
450+ } else if ("getOption" .equals (methodName )) {
451+ if (PhpElementsUtil .isMethodReferenceInstanceOf (methodCall , "\\ Symfony\\ Component\\ Console\\ Input\\ InputInterface" , "getOption" )) {
452+ callsToReplace .add (methodCall );
453+ }
454+ }
455+ }
456+
457+ // Replace each call
458+ for (MethodReference methodCall : callsToReplace ) {
459+ String methodName = methodCall .getName ();
460+ if (methodName == null ) {
461+ continue ;
462+ }
463+
464+ // Get the argument/option name from the first parameter
465+ PsiElement nameParam = PsiElementUtils .getMethodParameterPsiElementAt (methodCall , 0 );
466+ if (nameParam == null ) {
467+ continue ;
468+ }
469+
470+ String paramName = PhpElementsUtil .getStringValue (nameParam );
471+ if (StringUtils .isBlank (paramName )) {
472+ continue ;
473+ }
474+
475+ // Get the variable name from our map
476+ String variableName ;
477+ if ("getArgument" .equals (methodName )) {
478+ variableName = argumentVariableMap .get (paramName );
479+ } else {
480+ variableName = optionVariableMap .get (paramName );
481+ }
482+
483+ if (variableName == null ) {
484+ continue ;
485+ }
486+
487+ // Check if the method call is wrapped in a type cast
488+ PsiElement parent = methodCall .getParent ();
489+ PsiElement elementToReplace = methodCall ;
490+
491+ if (parent instanceof UnaryExpression ) {
492+ UnaryExpression unaryExpr = (UnaryExpression ) parent ;
493+ // Check if this is a cast expression by looking at the operator
494+ PsiElement operator = unaryExpr .getOperation ();
495+ if (operator != null ) {
496+ String operatorText = operator .getText ();
497+ // Common PHP type casts: (int), (string), (bool), (array), (object), (float), (double)
498+ if (operatorText .matches ("\\ (\\ s*(int|integer|string|bool|boolean|array|object|float|double|real)\\ s*\\ )" )) {
499+ elementToReplace = unaryExpr ;
500+ }
501+ }
502+ }
503+
504+ // Create a new variable reference
505+ PhpPsiElement newVariable = PhpPsiElementFactory .createFromText (
506+ invokeMethod .getProject (),
507+ Variable .class ,
508+ "$" + variableName
509+ );
510+
511+ if (newVariable != null ) {
512+ elementToReplace .replace (newVariable );
513+ }
514+ }
515+
516+ // Remove redundant self-assignments like $var = $var
517+ removeRedundantSelfAssignments (body );
518+ }
519+
520+ /**
521+ * Removes redundant self-assignments like $var = $var.
522+ * These occur when we replace $input->getArgument('name') with $name,
523+ * and the original code had $name = $input->getArgument('name').
524+ */
525+ private void removeRedundantSelfAssignments (@ NotNull GroupStatement body ) {
526+ Collection <AssignmentExpression > assignments = PsiTreeUtil .findChildrenOfType (body , AssignmentExpression .class );
527+ List <PsiElement > statementsToRemove = new ArrayList <>();
528+
529+ for (AssignmentExpression assignment : assignments ) {
530+ PhpPsiElement variable = assignment .getVariable ();
531+ PhpPsiElement value = assignment .getValue ();
532+
533+ // Check if both left and right side are variables
534+ if (variable instanceof Variable && value instanceof Variable ) {
535+ String leftVarName = ((Variable ) variable ).getName ();
536+ String rightVarName = ((Variable ) value ).getName ();
537+
538+ // If they're the same variable, mark the statement for removal
539+ if (leftVarName != null && leftVarName .equals (rightVarName )) {
540+ // Navigate up to find the statement containing this assignment
541+ PsiElement statement = PsiTreeUtil .getParentOfType (assignment , com .jetbrains .php .lang .psi .elements .Statement .class );
542+ if (statement != null && !statementsToRemove .contains (statement )) {
543+ statementsToRemove .add (statement );
544+ }
545+ }
546+ }
547+ }
548+
549+ // Remove all redundant statements
550+ for (PsiElement statement : statementsToRemove ) {
551+ statement .delete ();
552+ }
553+ }
554+
349555 private void reformatMethod (@ NotNull Project project , @ NotNull Method method ) {
350556 // Reformat only the method signature (parameters), not the entire body
351557 // Use CodeStyleManager directly to avoid creating extra undo events
0 commit comments