The Eclipse environment with Xtext provides an astoundingly powerful toolset. The EMF/TMF folks are doing a great job providing good capabilities with straightforward means of tailoring and extension. That said, there's a lot to the environment that can make some tasks appear more daunting than need be.
After reading Sven Efftinge's post on an experimental Xtext feature that automates linking among models in a project, I was definitely inclined to make use of it. On the other hand, it's a solution much bigger than the problem that I currently face: linking to references within the same model (program, actually) while assuming that references without a local definition are external.
My grammar allows references to imported constructs in a manner similar to Java's implied import of all class names that reside in the same package. The approach in Sven's article on integrating with the EMF index will ultimately be implemented if time allows in this project. Presently, I don't want the parser to actually load all the models partly because the main goal is to analyze individual models and the actual content of imported models is extraneous.
The simple way keep errors from being flagged where implicit imports can't be resolved is to extend the DefaultLinkingService. To start with, the ILinkingService binding has be be replaced by adding an override in the LanguageRuntimeModule class in the main parser project's
public Class<? extends ILinkingService> bindILinkingService() {
return PdlLinkingService.class;
}
The custom linking service is a minimal extension of DefaultLinkingService. Thanks to Sebastian for guidance on optimizing the integration to Xtext. Knut's pointers for dealing with the ECore resources was crucial to implementing a proper solution. Here's the complete class (I'm sure there's still room for improvement):
/** * Provide the linking semantics for PDL. The references to 'imported' table * definitions are stubbed out by this class until the Indexer is implemented. * * @author John Bito */ public class PdlLinkingService extends DefaultLinkingService { private PdlFactory factoryInstance = null; /** * Keep stubs so that new ones aren't created for each linking pass. */ private final Map<String, PhysicalFileName> stubbedRefs; private Resource stubsResource = null; public PdlLinkingService() { super(); stubbedRefs = new Hashtable<String, PhysicalFileName>(); } /** * Retrieve the factory for model elements * * @return the factory for the model defined by the language */ private PdlFactory getFactory() { if (null == factoryInstance) factoryInstance = PdlPackage.eINSTANCE.getPdlFactory(); return factoryInstance; } /** * Use a temporary 'child' resource to hold created stubs. The real resource * URI is used to generate a 'temporary' resource to be the container for * stub EObjects. * * @param source * the real resource that is being parsed * @return the cached reference to a resource named by the real resource * with the added extension 'xmi' */ private Resource makeResource(Resource source) { if (null != stubsResource) return stubsResource; URI stubURI = source.getURI(); stubURI = stubURI.appendFileExtension("xmi"); stubsResource = source.getResourceSet().getResource(stubURI, false); if (null == stubsResource) // TODO find out if this should be cleaned up so as not to clutter // the project. source.getResourceSet().createResource(stubURI); return stubsResource; } /** * Override default in order to supply a stub object. If the default * implementation isn't able to resolve the link, assume it to be a local * resource. * * @param context * the model element containing the reference * @param ref * the reference defining the type that must be resolved * @param node * the parse tree node containing the text of the reference (ID) * @return the default implementation's return if non-empty or else an * internally-generated PhysicalFileName * @throws IllegalNodeException * if detected by the default implementation * @see org.eclipse.xtext.linking.impl.DefaultLinkingService#getLinkedObjects(org.eclipse.emf.ecore.EObject, * org.eclipse.emf.ecore.EReference, * org.eclipse.xtext.parsetree.AbstractNode) */ @Override public List<EObject> getLinkedObjects(EObject context, EReference ref, AbstractNode node) throws IllegalNodeException { List<EObject> result = super.getLinkedObjects(context, ref, node); // If the default implementation resolved the link, return it if (null != result && !result.isEmpty()) return result; // Is this a reference to be stubbed? if (PdlPackage.Literals.FILE_NAME .isSuperTypeOf(ref.getEReferenceType())) { // Get the stub's name from the text of the parse tree node. String name = getCrossRefNodeAsString(node); FileName stub = stubbedRefs.get(name); if (null == stub) { // Create the model element instance using the factory stub = getFactory().createPhysicalFileName(); stub.setName(name); // Attach the stub to the resource that's being parsed makeResource(context.eResource()).getContents().add(stub); } result = Collections.singletonList((EObject) stub); } return result; } }
Of course, this is a stop-gap that's not appropriate for complete language processing, but it's illustrative of one simple way semantics may be adjusted within the Xtext framework. There are two likely problems making this implementation sub-optimal:
- The 'temporary' resource lifetime isn't managed, so the local cache of EObject references can be out of sync. Intuitively, one would expect the resource to exist longer than the LinkingService, so it will be surprising to find the LinkingService using a reference to an EObject that's not stored in the resource.
- Since the code doesn't try to find EObject instances that are already in the resource, there can be a memory leak if the LinkingService is created multiple times during a session. It may make sense to use the delete method on the Resource, but it looks a bit dangerous and I don't have the wherewithal to test that at the moment.