Extending PDT

From Eclipsepedia

(Redirected from Extending PDT 2.2)
Jump to: navigation, search

Contents

Purpose

There are different purposes for extending PDT. One of them is adding support for specific PHP framework to the IDE features like: Code Assist, Navigation (CTRL + click), Presentation (Outline, PHP Explorer). In this document we'll describe how to achieve these goals using PDT extension points. If you have any questions regarding this document, please ask them on PDT-Dev mailing list.

Extending

Code Assist

Type inference hinting

Suppose your framework uses the following language structure for object instantiation:

$myObject = ClassRegistry::init('MyClass');

In this case PDT type inference engine is unable to detect the type of $myObject variable, so we'll have to add a specific rule that helps him. The following extension point allows to provide additional rules to the PHP type inference engine:

 org.eclipse.php.core.goalEvaluatorFactories

For our example what we need to contribute is:

<extension point="org.eclipse.php.core.goalEvaluatorFactories">
  <factory
      class="com.xyz.php.fmwrk.XYZGoalEvaluatorFactory"
      priority="100">
  </factory>
</extension>

Please note the priority is set to 100 in order to override the default PHP goal evaluator (its priority is 10).

public class XYZGoalEvaluatorFactory implements IGoalEvaluatorFactory {
 
  public GoalEvaluator createEvaluator(IGoal goal) {
    Class<?> goalClass = goal.getClass();
 
    // We're overriding only the expression type goal:
    if (goalClass == ExpressionTypeGoal.class) {
      ASTNode expression = ((ExpressionTypeGoal) goal).getExpression();
 
      // Check the expression AST node type
      if (expression instanceof StaticMethodInvocation) {
        StaticMethodInvocation inv = (StaticMethodInvocation) expression;
        ASTNode reciever = inv.getReceiver();
 
        // Check that the class name is 'CallRegistry':
        if (reciever instanceof SimpleReference
            && "ClassRegistry".equals(((SimpleReference) reciever)
                .getName())) {
 
          // Check that the method name is 'init'
          if ("init".equals(inv.getCallName().getName())) {
 
            // Take the first call argument:
            List arguments = inv.getArgs().getChilds();
            if (arguments.size() == 1) {
              Object first = arguments.get(0);
 
              if (first instanceof Scalar
                 && ((Scalar) first).getScalarType() == Scalar.TYPE_STRING) {
 
                String className = ((Scalar) first).getValue();
 
                // Return the evaluated type through dummy
                // evaluator
                return new DummyGoalEvaluator(goal, className);
              }
            }
          }
        }
      }
    }
 
    // Give the control to the default PHP goal evaluator
    return null;
  }
}
public class DummyGoalEvaluator extends GoalEvaluator {
  private String className;
 
  public DummyGoalEvaluator(IGoal goal, String className) {
    super(goal);
    this.className = className;
  }
 
  public Object produceResult() {
    return new PHPClassType(className);
  }
 
  public IGoal[] init() {
    return null;
  }
 
  public IGoal[] subGoalDone(IGoal subgoal, Object result,
      GoalState state) {
    return null;
  }
}

That's all. In case if 'MyClass' exists Code Assist and Navigation will work out of the box since they are both based on the type inference engine.

Code assist strategies

Some PHP frameworks provide class fields or methods that are not declared explicitly in a code. For instance, Zend Framework view helpers can be accessed from the view class using:

 $this->helperName()

Behind the scenes, view loads the Zend_View_Helper_HelperName class (note the naming convention), creates an object instance of it, and calls its helperName() method.

In order to provide Code Assist for "helperName" after "$this->" we'll need to extend the following extensions:

 org.eclipse.php.core.completionContextResolvers
org.eclipse.php.core.completionStrategyFactories

First of all, we need to define the completion context - a class that verifies that the cursor is positioned after the object call operator and that the object type is a View.

public class XYZCompletionContext extends ClassMemberContext {
 
  public boolean isValid(ISourceModule sourceModule, int offset,
      CompletionRequestor requestor) {
 
    // Call to super to verify that cursor is in the class member call
    // context
    if (super.isValid(sourceModule, offset, requestor)) {
 
      // This context only supports "->" trigger type (not the "::")
      if (getTriggerType() == Trigger.OBJECT) {
 
        IType[] recieverClass = getLhsTypes();
        // recieverClass contains types for the expression from the left
        // side of "->"
        for (IType c : recieverClass) {
          if (!isViewer(c)) {
            return false;
          }
        }
        return true;
      }
    }
 
    return false;
  }
 
  /**
   * Check that the type of the class is Viewer
   */
  private boolean isViewer(IType type) {
    // XXX: add more sophisticated check
    return "Viewer".equalsIgnoreCase(type.getElementName());
  }
}

Register new context in the completion engine:

public class XYZContextResolver extends CompletionContextResolver
    implements ICompletionContextResolver {
 
    public ICompletionContext[] createContexts() {
        return new ICompletionContext[] { new XYZCompletionContext(); };
    }
}

Add the completion strategy that does the actual job - adds "helperName" method to the completion list:

public class XYZCompletionStrategy
    extends ClassMembersStrategy implements ICompletionStrategy {
 
  public XYZCompletionStrategy(ICompletionContext context) {
    super(context);
  }
 
  public void apply(ICompletionReporter reporter) throws Exception {
    XYZCompletionContext context = (XYZCompletionContext) getContext();
    IType type = context.getLhsTypes()[0];
    for (String helperName : getHelperNames()) {
      // Create fake model element for the helper method
      FakeMethod fakeHelperMethod = new FakeMethod((ModelElement) type,
          helperName);
 
      // Report the method to the completion proposals list
      reporter.reportMethod(fakeHelperMethod, "()",
          getReplacementRange(context));
    }
  }
 
  private String[] getHelperNames() {
    // XXX: calculate existing helper names from code
    return new String[] { "helperName" };
  }
}

Create an association between the completion context and completion strategy:

 
 
public class XYZCompletionStrategyFactory implements ICompletionStrategyFactory {
 
  public ICompletionStrategy[] create(ICompletionContext[] contexts) {
    List<ICompletionStrategy> result = new LinkedList<ICompletionStrategy>();
    for (ICompletionContext context : contexts) {
      if (context.getClass() == XYZCompletionContext.class) {
        result.add(new XYZCompletionStrategy(context));
      }
    }
    return (ICompletionStrategy[]) result
        .toArray(new ICompletionStrategy[result.size()]);
  }
}

And, finally, plugin.xml contributions:

<extension point="org.eclipse.php.core.completionStrategyFactories">
    <factory class="com.xyz.php.framework.XYZCompletionStrategyFactory"/>
</extension>
 
<extension point="org.eclipse.php.core.completionContextResolvers">
    <resolver class="com.xyz.php.framework.XYZContextResolver"/>
</extension>


CTRL + click

Contributing to index

When you click while holding CTRL pressed on some PHP element in an editor, or when you ask for Code Assist by pressing CTRL+Space selection engine (or completion engine in case of Code Assist) tries to resolve PHP elements by accessing index. Index contains all the PHP element declarations and references that present in a workspace. The following example indexes non-existing select(), delete() and insert() methods from code snippet #1.

Extension point that we gonna use is:

 org.eclipse.php.core.phpIndexingVisitors

Implement the indexing visitor:

public class XYZIndexingVisitorExtension 
    extends PhpIndexingVisitorExtension {
 
  private ClassDeclaration currentClass;
  private MethodDeclaration currentMethod;
  private Set<Scalar> deferredMethods = new HashSet<Scalar>();
 
  public boolean visit(TypeDeclaration s) throws Exception {
    if (s instanceof ClassDeclaration) {
      currentClass = (ClassDeclaration) s;
    }
    return true;
  }
 
  public boolean endvisit(TypeDeclaration s) throws Exception {
    for (Scalar method : deferredMethods) {
      int start = method.sourceStart();
      int length = method.sourceEnd() - method.sourceStart();
      String name = method.getValue().replaceAll("['\"]", "");
 
      modifyDeclaration(method, new DeclarationInfo(IModelElement.METHOD,
          Modifiers.AccPublic, start, length, start, length, name,
          null, null, currentClass.getName()));
    }
    currentClass = null;
    deferredMethods.clear();
    return true;
  }
 
  public boolean visit(MethodDeclaration s) throws Exception {
    if (currentClass != null && "__call".equals(s.getName())) {
      currentMethod = s;
    }
    return true;
  }
 
  public boolean endvisit(MethodDeclaration s) throws Exception {
    currentMethod = null;
    return true;
  }
 
  public boolean visit(Statement s) throws Exception {
    if (s instanceof IfStatement && currentClass != null
        && currentMethod != null) {
 
      Expression condition = ((IfStatement) s).getCondition();
      if (condition instanceof InfixExpression) {
 
        InfixExpression infixExp = (InfixExpression) condition;
        if (infixExp.getOperatorType() == InfixExpression.OP_IS_EQUAL) {
 
          Expression rightExp = infixExp.getRight();
          if (rightExp instanceof Scalar) {
 
            Scalar scalar = (Scalar) rightExp;
            if (scalar.getScalarType() == Scalar.TYPE_STRING) {
              deferredMethods.add(scalar);
            }
          }
        }
      }
    }
    return true;
  }
}

Note: this visitor is very similar to the structured model contribution visitor.

Finally, register this class in plugin.xml:

 <extension point="org.eclipse.php.core.phpIndexingVisitors">
  <visitor class="com.xyz.php.framework.XYZIndexingVisitorExtension" />
</extension>

Outline and PHP Explorer

Adding additional nodes to the PHP explorer can be achieved in 2 different ways.

Contributing to the structured model

Sometimes you need to present some elements that don't really exist. For example, you may want to present fields and methods that are processed using __get() or __call() magic methods:

Code snippet #1

/**
 * Please don't judge me for this code, it's for the sake of example only
 * (even though I've seen PHP code like this in real PHP applications :-])
 */
class MySQL_DB {
 
   public function __call($name, $arguments) {
      if ($name == "select") {
         mysql_query($this->db, "SELECT * FROM ".$arguments[0]);
         // ...
      }
      else if ($name == "insert") {
         // ...
      }
      else if ($name == "delete") {
         // ...
      }
   }
}

Let's say we wish to see select(), insert() and delete() methods in the Outline and PHP Explorer under MySQL_DB class. We'll need to extend the following extension point:

 org.eclipse.php.core.phpSourceElementRequestors

plugin.xml contribution:

 <extension point="org.eclipse.php.core.phpSourceElementRequestors">
    <requestor
          class="com.xyz.php.framework.XYZSourceElementRequestor">
    </requestor>
 </extension>

Source element requester extension implementation:

 
 
public class XYZSourceElementRequestor extends
    PHPSourceElementRequestorExtension {
 
  private ClassDeclaration currentClass;
  private MethodDeclaration currentMethod;
  private Set<Scalar> deferredMethods = new HashSet<Scalar>();
 
  public boolean visit(TypeDeclaration s) throws Exception {
    if (s instanceof ClassDeclaration) {
      currentClass = (ClassDeclaration) s;
    }
    return true;
  }
 
  public boolean endvisit(TypeDeclaration s) throws Exception {
    currentClass = null;
 
    for (Scalar method : deferredMethods) {
      ISourceElementRequestor.MethodInfo methodInfo =
        new ISourceElementRequestor.MethodInfo();
 
      methodInfo.name = method.getValue().replaceAll("['\"]", "");
      methodInfo.modifiers = Modifiers.AccPublic;
      methodInfo.nameSourceStart = method.sourceStart();
      methodInfo.nameSourceEnd = method.sourceEnd();
      methodInfo.declarationStart = method.sourceStart();
      fRequestor.enterMethod(methodInfo);
      fRequestor.exitMethod(method.sourceEnd());
    }
    deferredMethods.clear();
    return true;
  }
 
  public boolean visit(MethodDeclaration s) throws Exception {
    if (currentClass != null && "__call".equals(s.getName())) {
      currentMethod = s;
    }
    return true;
  }
 
  public boolean endvisit(MethodDeclaration s) throws Exception {
    currentMethod = null;
    return true;
  }
 
  public boolean visit(Statement s) throws Exception {
    if (s instanceof IfStatement && currentClass != null
        && currentMethod != null) {
 
      Expression condition = ((IfStatement) s).getCondition();
      if (condition instanceof InfixExpression) {
 
        InfixExpression infixExp = (InfixExpression) condition;
        if (infixExp.getOperatorType() == InfixExpression.OP_IS_EQUAL) {
 
          Expression rightExp = infixExp.getRight();
          if (rightExp instanceof Scalar) {
 
            Scalar scalar = (Scalar) rightExp;
            if (scalar.getScalarType() == Scalar.TYPE_STRING) {
              deferredMethods.add(scalar);
            }
          }
        }
      }
    }
    return true;
  }
}

Voila! Methods are added into PHP Explorer and Outline views, and even clicking on them changes focus in the editor:

Requestor1.png


Contributing a PHPTreeContentProvider

Since PDT 3.1.1. additional nodes can be added to the PHP explorer using the org.eclipse.php.ui.phpTreeContentProviders extension point. Let's say you want

to contribute a custom buildpath container using the phpTreeContentProviders extension point:

<extension point="org.eclipse.php.ui.phpTreeContentProviders">
    <provider labelProvider="com.dubture.composer.core.ui.explorer.PackageTreeLabelProvider" contentProvider="com.dubture.composer.core.ui.explorer.PackageTreeContentProvider"/>
</extension>

All you need to do is to create the labelProvider and contentProvider elements, by implementing an org.eclipse.jface.viewers.ILabelProvider and an org.eclipse.jface.viewers.ITreeContentProvider:


public class PackageTreeLabelProvider extends LabelProvider
{
 
    @Override
    public String getText(Object element)
    {
        if (element instanceof PackagePath) {
            PackagePath path = (PackagePath) element;
            return path.getPackageName();
        }
 
        return null;
    }
 
    @Override
    public Image getImage(Object element)
    {
        if (element instanceof PackagePath) {
            return PHPPluginImages
                    .get(PHPPluginImages.IMG_OBJS_LIBRARY);
        }
 
        return null;
    }
}


public class PackageTreeContentProvider extends ScriptExplorerContentProvider
{
    public PackageTreeContentProvider()
    {
        super(true);
    }
 
 
    @Override
    public Object[] getChildren(Object parentElement)
    {
        if (parentElement instanceof PackagePath) {
 
            PackagePath pPath = (PackagePath) parentElement;
            IScriptProject scriptProject = pPath.getProject();
            IBuildpathEntry entry = pPath.getEntry();
 
            try {
 
                IProjectFragment[] allProjectFragments;
                allProjectFragments = scriptProject.getAllProjectFragments();
                for (IProjectFragment fragment : allProjectFragments) {
                    if (fragment instanceof ExternalProjectFragment) {
                        ExternalProjectFragment external = (ExternalProjectFragment) fragment;
                        if (external.getBuildpathEntry().equals(entry)) {
                            return super.getChildren(external);
                        }
                    }
                }
            } catch (ModelException e) {
                Logger.logException(e);
            }
        } else if (parentElement instanceof ComposerBuildpathContainer) {
            ComposerBuildpathContainer container = (ComposerBuildpathContainer) parentElement;
 
            IAdaptable[] children = container.getChildren();
 
            if (children == null || children.length == 0) {
                return NO_CHILDREN;
            }
 
            return children;
        } else if (parentElement instanceof IScriptProject) {
            try {
                IProject project = ((IScriptProject)parentElement).getProject();
                if (project.hasNature(ComposerNature.NATURE_ID)) {
                    return new Object[]{new ComposerBuildpathContainer((IScriptProject) parentElement)};
                }
            } catch (Exception e) {
                Logger.logException(e);
            }
        }
        return null;
    }
}


PhpTreeContentProvider.png

The example has been taken from the Composer Eclipse Plugin


Language library contributions

PHP language library contains builtin classes, functions and constants - you can see it under PHP project as a "PHP Language Library" node (see image above). This model is used in Code Assist and PHP Function View. By default, it contains only predefined set of PHP extensions. There's a way to contribute more PHP language fragments to this library. The key extension point is:

 org.eclipse.php.core.languageModelProviders

Let's say we need to add PHP-newt extension functions to the Code Assist. First, create the language model provider class:

public class NewtLanguageModelProvider 
    implements ILanguageModelProvider {
 
  public IPath getPath(IScriptProject project) {
    return new Path("resources/newt");
  }
 
  public Plugin getPlugin() {
    return MyPlugin.getDefault();
  }
}

Please not that this interface has been changed since PDT 2.1 according to bug 291729

Register this class in plugin.xml:

 <extension point="org.eclipse.php.core.languageModelProviders">
  <provider class="com.xyz.php.framework.NewtLanguageModelProvider" />
</extension>

Create resources/newt folder in your plug-in directory, and add this folder to the build.properties, then place PHP stub files under this folder.

Error reporting / validation

Sometimes there's a need to add custom PHP source code validator that reports additional errors or warnings to user. Please learn how to do that from this plugin. This example adds validator that checks PHP code for places possibly vulnerable for XSS attack, and reports warnings to developer.


Providing quickfixes for your validation problems

Showing validation errors is fine, but helping out with a quickfix is even better:

You can use the org.eclipse.php.ui.quickFixProcessors extension point to do this. See the PDT Extensions plugin for an example implementation.