Skip to main content
Jump to: navigation, search

Difference between revisions of "Extending PDT"

(Type inference hinting)
Line 1: Line 1:
== Purpose ==
+
== 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 [mailto:pdt-dev@eclipse.org PDT-Dev] mailing list.
+
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 [mailto:pdt-dev@eclipse.org PDT-Dev] mailing list.  
 +
 
 +
== Extending  ==
 +
 
 +
=== Code Assist  ===
  
== Extending ==
 
=== Code Assist ===
 
 
==== Type inference hinting  ====
 
==== Type inference hinting  ====
Suppose your framework uses the following language structure for object instantiation:
+
 
 +
Suppose your framework uses the following language structure for object instantiation:  
  
 
<source lang="php">
 
<source lang="php">
 
$myObject = ClassRegistry::init('MyClass');
 
$myObject = ClassRegistry::init('MyClass');
</source>
+
</source>  
  
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:
+
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
 
   org.eclipse.php.core.goalEvaluatorFactories
  
For our example what we need to contribute is:
+
For our example what we need to contribute is:  
  
 
<source lang="xml">
 
<source lang="xml">
Line 25: Line 28:
 
   </factory>
 
   </factory>
 
</extension>
 
</extension>
</source>
+
</source>  
  
Please note the priority is set to 100 in order to override the default PHP goal evaluator (its priority is 10).
+
Please note the priority is set to 100 in order to override the default PHP goal evaluator (its priority is 10).  
  
 
<source lang="java">
 
<source lang="java">
Line 76: Line 79:
 
   }
 
   }
 
}
 
}
</source>
+
</source>  
 
+
<pre>public class DummyGoalEvaluator extends GoalEvaluator {
<pre>
+
public class DummyGoalEvaluator extends GoalEvaluator {
+
 
   private String className;
 
   private String className;
  
Line 100: Line 101:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 +
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.
  
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  ====
  
==== 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:  
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()
+
   $this-&gt;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.
+
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:
+
In order to provide Code Assist for "helperName" after "$this-&gt;" we'll need to extend the following extensions:  
  
 
   org.eclipse.php.core.completionContextResolvers
 
   org.eclipse.php.core.completionContextResolvers
  org.eclipse.php.core.completionStrategyFactories
+
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.
+
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.  
 
+
<pre>public class XYZCompletionContext extends ClassMemberContext {
<pre>
+
public class XYZCompletionContext extends ClassMemberContext {
+
  
 
   public boolean isValid(ISourceModule sourceModule, int offset,
 
   public boolean isValid(ISourceModule sourceModule, int offset,
Line 128: Line 127:
 
     if (super.isValid(sourceModule, offset, requestor)) {
 
     if (super.isValid(sourceModule, offset, requestor)) {
  
       // This context only supports "->" trigger type (not the "::")
+
       // This context only supports "-&gt;" trigger type (not the "::")
 
       if (getTriggerType() == Trigger.OBJECT) {
 
       if (getTriggerType() == Trigger.OBJECT) {
  
 
         IType[] recieverClass = getLhsTypes();
 
         IType[] recieverClass = getLhsTypes();
 
         // recieverClass contains types for the expression from the left
 
         // recieverClass contains types for the expression from the left
         // side of "->"
+
         // side of "-&gt;"
         for (IType c : recieverClass) {
+
         for (IType c&nbsp;: recieverClass) {
 
           if (!isViewer(c)) {
 
           if (!isViewer(c)) {
 
             return false;
 
             return false;
Line 154: Line 153:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 
+
Register new context in the completion engine:  
Register new context in the completion engine:
+
<pre>public class XYZContextResolver extends CompletionContextResolver
 
+
<pre>
+
public class XYZContextResolver extends CompletionContextResolver
+
 
     implements ICompletionContextResolver {
 
     implements ICompletionContextResolver {
  
Line 166: Line 162:
 
     }
 
     }
 
}
 
}
</pre>
+
</pre>  
 
+
Add the completion strategy that does the actual job - adds "helperName" method to the completion list:  
Add the completion strategy that does the actual job - adds "helperName" method to the completion list:
+
<pre>public class XYZCompletionStrategy
 
+
<pre>
+
public class XYZCompletionStrategy
+
 
     extends ClassMembersStrategy implements ICompletionStrategy {
 
     extends ClassMembersStrategy implements ICompletionStrategy {
  
Line 181: Line 174:
 
     XYZCompletionContext context = (XYZCompletionContext) getContext();
 
     XYZCompletionContext context = (XYZCompletionContext) getContext();
 
     IType type = context.getLhsTypes()[0];
 
     IType type = context.getLhsTypes()[0];
     for (String helperName : getHelperNames()) {
+
     for (String helperName&nbsp;: getHelperNames()) {
 
       // Create fake model element for the helper method
 
       // Create fake model element for the helper method
 
       FakeMethod fakeHelperMethod = new FakeMethod((ModelElement) type,
 
       FakeMethod fakeHelperMethod = new FakeMethod((ModelElement) type,
Line 197: Line 190:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 
+
Create an association between the completion context and completion strategy:  
Create an association between the completion context and completion strategy:
+
<pre>public class XYZCompletionStrategyFactory implements ICompletionStrategyFactory {
 
+
<pre>
+
public class XYZCompletionStrategyFactory implements ICompletionStrategyFactory {
+
  
 
   public ICompletionStrategy[] create(ICompletionContext[] contexts) {
 
   public ICompletionStrategy[] create(ICompletionContext[] contexts) {
     List<ICompletionStrategy> result = new LinkedList<ICompletionStrategy>();
+
     List&lt;ICompletionStrategy&gt; result = new LinkedList&lt;ICompletionStrategy&gt;();
     for (ICompletionContext context : contexts) {
+
     for (ICompletionContext context&nbsp;: contexts) {
 
       if (context.getClass() == XYZCompletionContext.class) {
 
       if (context.getClass() == XYZCompletionContext.class) {
 
         result.add(new XYZCompletionStrategy(context));
 
         result.add(new XYZCompletionStrategy(context));
Line 215: Line 205:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 +
And, finally, plugin.xml contributions:
  
And, finally, plugin.xml contributions:
+
  &lt;extension point="org.eclipse.php.core.completionStrategyFactories"&gt;
 +
    &lt;factory class="com.xyz.php.framework.XYZCompletionStrategyFactory"/&gt;
 +
  &lt;/extension&gt;
 +
 
 +
  &lt;extension point="org.eclipse.php.core.completionContextResolvers"&gt;
 +
    &lt;resolver class="com.xyz.php.framework.XYZContextResolver"/&gt;
 +
  &lt;/extension&gt;
  
  <extension point="org.eclipse.php.core.completionStrategyFactories">
+
=== CTRL + click  ===
      <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  ====
  
==== 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 [[Extending PDT 2.2#Snippet1|code snippet #1]].  
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 [[Extending_PDT_2.2#Snippet1|code snippet #1]].
+
  
Extension point that we gonna use is:
+
Extension point that we gonna use is:  
  
 
   org.eclipse.php.core.phpIndexingVisitors
 
   org.eclipse.php.core.phpIndexingVisitors
  
Implement the indexing visitor:
+
Implement the indexing visitor:  
 
+
<pre>public class XYZIndexingVisitorExtension  
<pre>
+
public class XYZIndexingVisitorExtension  
+
 
     extends PhpIndexingVisitorExtension {
 
     extends PhpIndexingVisitorExtension {
  
 
   private ClassDeclaration currentClass;
 
   private ClassDeclaration currentClass;
 
   private MethodDeclaration currentMethod;
 
   private MethodDeclaration currentMethod;
   private Set<Scalar> deferredMethods = new HashSet<Scalar>();
+
   private Set&lt;Scalar&gt; deferredMethods = new HashSet&lt;Scalar&gt;();
  
 
   public boolean visit(TypeDeclaration s) throws Exception {
 
   public boolean visit(TypeDeclaration s) throws Exception {
Line 254: Line 242:
  
 
   public boolean endvisit(TypeDeclaration s) throws Exception {
 
   public boolean endvisit(TypeDeclaration s) throws Exception {
     for (Scalar method : deferredMethods) {
+
     for (Scalar method&nbsp;: deferredMethods) {
 
       int start = method.sourceStart();
 
       int start = method.sourceStart();
 
       int length = method.sourceEnd() - method.sourceStart();
 
       int length = method.sourceEnd() - method.sourceStart();
Line 269: Line 257:
  
 
   public boolean visit(MethodDeclaration s) throws Exception {
 
   public boolean visit(MethodDeclaration s) throws Exception {
     if (currentClass != null && "__call".equals(s.getName())) {
+
     if (currentClass&nbsp;!= null &amp;&amp; "__call".equals(s.getName())) {
 
       currentMethod = s;
 
       currentMethod = s;
 
     }
 
     }
Line 281: Line 269:
  
 
   public boolean visit(Statement s) throws Exception {
 
   public boolean visit(Statement s) throws Exception {
     if (s instanceof IfStatement && currentClass != null
+
     if (s instanceof IfStatement &amp;&amp; currentClass&nbsp;!= null
         && currentMethod != null) {
+
         &amp;&amp; currentMethod&nbsp;!= null) {
  
 
       Expression condition = ((IfStatement) s).getCondition();
 
       Expression condition = ((IfStatement) s).getCondition();
Line 304: Line 292:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 +
Note: this visitor is very similar to the structured model contribution [[Extending PDT 2.2#SModelVisitor|visitor]].
  
Note: this visitor is very similar to the structured model contribution [[Extending_PDT_2.2#SModelVisitor|visitor]].
+
Finally, register this class in plugin.xml:
  
Finally, register this class in plugin.xml:
+
  &lt;extension point="org.eclipse.php.core.phpIndexingVisitors"&gt;
 +
  &lt;visitor class="com.xyz.php.framework.XYZIndexingVisitorExtension" /&gt;
 +
&lt;/extension&gt;
  
  <extension point="org.eclipse.php.core.phpIndexingVisitors">
+
=== Outline and PHP Explorer  ===
    <visitor class="com.xyz.php.framework.XYZIndexingVisitorExtension" />
+
  </extension>
+
  
=== Outline and PHP Explorer ===
 
==== 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:
 
  
<span id="Snippet1">Code snippet #1</span>
+
 
<pre>
+
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:
 +
 
 +
<span id="Snippet1">Code snippet #1</span>  
 +
<pre>/**
 
  * Please don't judge me for this code, it's for the sake of example only
 
  * 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 :-])
+
  * (even though I've seen PHP code like this in real PHP applications&nbsp;:-])
 
  */
 
  */
 
class MySQL_DB {
 
class MySQL_DB {
Line 328: Line 320:
 
   public function __call($name, $arguments) {
 
   public function __call($name, $arguments) {
 
       if ($name == "select") {
 
       if ($name == "select") {
         mysql_query($this->db, "SELECT * FROM ".$arguments[0]);
+
         mysql_query($this-&gt;db, "SELECT * FROM ".$arguments[0]);
 
         // ...
 
         // ...
 
       }
 
       }
Line 339: Line 331:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
 
+
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:  
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
 
   org.eclipse.php.core.phpSourceElementRequestors
  
plugin.xml contribution:
+
plugin.xml contribution:  
  
   <extension point="org.eclipse.php.core.phpSourceElementRequestors">
+
   &lt;extension point="org.eclipse.php.core.phpSourceElementRequestors"&gt;
      <requestor
+
    &lt;requestor
            class="com.xyz.php.framework.XYZSourceElementRequestor">
+
          class="com.xyz.php.framework.XYZSourceElementRequestor"&gt;
      </requestor>
+
    &lt;/requestor&gt;
  </extension>
+
  &lt;/extension&gt;
  
 
<span id="SModelVisitor">
 
<span id="SModelVisitor">
 
Source element requester extension implementation:
 
Source element requester extension implementation:
</span>
+
</span>  
 
+
<pre>public class XYZSourceElementRequestor extends
<pre>
+
public class XYZSourceElementRequestor extends
+
 
     PHPSourceElementRequestorExtension {
 
     PHPSourceElementRequestorExtension {
  
 
   private ClassDeclaration currentClass;
 
   private ClassDeclaration currentClass;
 
   private MethodDeclaration currentMethod;
 
   private MethodDeclaration currentMethod;
   private Set<Scalar> deferredMethods = new HashSet<Scalar>();
+
   private Set&lt;Scalar&gt; deferredMethods = new HashSet&lt;Scalar&gt;();
  
 
   public boolean visit(TypeDeclaration s) throws Exception {
 
   public boolean visit(TypeDeclaration s) throws Exception {
Line 375: Line 364:
 
     currentClass = null;
 
     currentClass = null;
  
     for (Scalar method : deferredMethods) {
+
     for (Scalar method&nbsp;: deferredMethods) {
 
       ISourceElementRequestor.MethodInfo methodInfo =
 
       ISourceElementRequestor.MethodInfo methodInfo =
 
         new ISourceElementRequestor.MethodInfo();
 
         new ISourceElementRequestor.MethodInfo();
Line 392: Line 381:
  
 
   public boolean visit(MethodDeclaration s) throws Exception {
 
   public boolean visit(MethodDeclaration s) throws Exception {
     if (currentClass != null && "__call".equals(s.getName())) {
+
     if (currentClass&nbsp;!= null &amp;&amp; "__call".equals(s.getName())) {
 
       currentMethod = s;
 
       currentMethod = s;
 
     }
 
     }
Line 404: Line 393:
  
 
   public boolean visit(Statement s) throws Exception {
 
   public boolean visit(Statement s) throws Exception {
     if (s instanceof IfStatement && currentClass != null
+
     if (s instanceof IfStatement &amp;&amp; currentClass&nbsp;!= null
         && currentMethod != null) {
+
         &amp;&amp; currentMethod&nbsp;!= null) {
  
 
       Expression condition = ((IfStatement) s).getCondition();
 
       Expression condition = ((IfStatement) s).getCondition();
Line 427: Line 416:
 
   }
 
   }
 
}
 
}
 +
</pre>
 +
Voila! Methods are added into PHP Explorer and Outline views, and even clicking on them changes focus in the editor:
 +
 +
[[Image: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:
 +
 +
<pre>
 +
 +
  <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>
 +
 
</pre>
 
</pre>
  
Voila! Methods are added into PHP Explorer and Outline views, and even clicking on them changes focus in the editor:
+
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:
  
[[Image:Requestor1.png]]
 
  
=== Language library contributions ===
+
<pre>
  
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:
+
public class PackageTreeLabelProvider extends LabelProvider
 +
{
  
  org.eclipse.php.core.languageModelProviders
+
    @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;
 +
    }
 +
}
 +
 
 +
</pre>
  
Let's say we need to add [http://pecl.php.net/package/newt PHP-newt] extension functions to the Code Assist. First, create the language model provider class:
 
  
 
<pre>
 
<pre>
public class NewtLanguageModelProvider  
+
 
 +
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;
 +
    }
 +
}
 +
 
 +
</pre>
 +
 
 +
 
 +
 
 +
===  ===
 +
 
 +
=== 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 [http://pecl.php.net/package/newt PHP-newt] extension functions to the Code Assist. First, create the language model provider class:
 +
<pre>public class NewtLanguageModelProvider  
 
     implements ILanguageModelProvider {
 
     implements ILanguageModelProvider {
  
Line 453: Line 555:
 
   }
 
   }
 
}
 
}
</pre>
+
</pre>  
Please not that this interface has been changed since PDT 2.1 according to [https://bugs.eclipse.org/bugs/show_bug.cgi?id=291729 bug 291729]
+
Please not that this interface has been changed since PDT 2.1 according to [https://bugs.eclipse.org/bugs/show_bug.cgi?id=291729 bug 291729]  
  
Register this class in plugin.xml:
+
Register this class in plugin.xml:  
  
   <extension point="org.eclipse.php.core.languageModelProviders">
+
   &lt;extension point="org.eclipse.php.core.languageModelProviders"&gt;
    <provider class="com.xyz.php.framework.NewtLanguageModelProvider" />
+
  &lt;provider class="com.xyz.php.framework.NewtLanguageModelProvider" /&gt;
  </extension>
+
&lt;/extension&gt;
  
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.
+
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 ===
+
=== Error reporting ===
  
 
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 [http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.pdt/examples/org.eclipse.php.examples.xss/?root=Tools_Project this plugin]. This example adds validator that checks PHP code for places possibly vulnerable for XSS attack, and reports warnings to developer.
 
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 [http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.pdt/examples/org.eclipse.php.examples.xss/?root=Tools_Project this plugin]. This example adds validator that checks PHP code for places possibly vulnerable for XSS attack, and reports warnings to developer.

Revision as of 03:18, 10 October 2012

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 theorg.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;
    }
}


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

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.

Back to the top