From a489961a3f14faed4bd3bedc7bdda0011e0eb4fc Mon Sep 17 00:00:00 2001 From: Chris Llanwarne Date: Mon, 16 Oct 2017 10:04:25 -0400 Subject: [PATCH] Reference resolution from values to declarations --- README.md | 11 ++- src/winstanley/WdlAnnotator.scala | 19 ++--- src/winstanley/psi/WdlNamedElement.scala | 7 ++ .../psi/impl/WdlNamedElementImpl.scala | 11 +++ src/winstanley/psi/impl/WdlPsiImplUtil.scala | 34 +++++++++ .../references/WdlDeclarationReference.scala | 41 +++++++++++ src/winstanley/structure/WdlImplicits.scala | 72 +++++++------------ src/winstanley/wdl.bnf | 10 ++- 8 files changed, 140 insertions(+), 65 deletions(-) create mode 100644 src/winstanley/psi/WdlNamedElement.scala create mode 100644 src/winstanley/psi/impl/WdlNamedElementImpl.scala create mode 100644 src/winstanley/psi/impl/WdlPsiImplUtil.scala create mode 100644 src/winstanley/references/WdlDeclarationReference.scala diff --git a/README.md b/README.md index ac78f12..b021ef6 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,13 @@ This plug-in currently supports: * Syntax highlighting +* Collapsible code blocks for workflows, tasks, and more. +* Highlighting and completion of parentheses `()` and other braces. +* Allows auto-commenting of lines in WDL with CMD+/ +* Undeclared value detection +* "Go to declaration" (currently for declared values only) -Winstanley is open sourced under the BSD 3-Clause license. +More features will be coming soon! ## Getting Started @@ -24,8 +29,8 @@ To build or test the plugin using IntelliJ: 4. Make sure the repo has a valid Scala SDK attached as a project dependency. * Otherwise you'll see errors like `"Cannot find class WdlElementType"` even though it's clearly there! 5. Generate the necessary files (on Mac): - * Navigate to Wdl.flex and generate sources using \[Command + Shift + G\] - * Navigate to wdl.bnf and generate sources using \[Command + Shift + G\] + * Navigate to Wdl.flex and generate sources using CMD+SHIFT+G. + * Navigate to wdl.bnf and generate sources using CMD+SHIFT+G 6. At this point, you can run or test the project using IntelliJ's preset run modes. * Open the `Run Configurations` window (for me, in the top right of the IntelliJ window) * Add a new one configuration with the `+` icon. diff --git a/src/winstanley/WdlAnnotator.scala b/src/winstanley/WdlAnnotator.scala index 5c41069..0edffbb 100644 --- a/src/winstanley/WdlAnnotator.scala +++ b/src/winstanley/WdlAnnotator.scala @@ -2,27 +2,20 @@ package winstanley import com.intellij.lang.annotation.{AnnotationHolder, Annotator} import com.intellij.psi.PsiElement -import winstanley.psi.WdlValue +import winstanley.psi.WdlVariableLookup import winstanley.structure.WdlImplicits._ class WdlAnnotator extends Annotator { override def annotate(psiElement: PsiElement, annotationHolder: AnnotationHolder): Unit = psiElement match { - case value: WdlValue => + case value: WdlVariableLookup => // If this value is an identifier, make sure that it's been declared somewhere (either in a declaration or in a scatter) - value.asIdentifierNode foreach { identifier => - val declarationNames = value.findDeclarationsAvailableInScope.map(_.declaredValueName) collect { case Some(d) => d } - - val scatterVariableName = for { - outerscatter <- value.findContainingScatter - scatterVariable <- outerscatter.getIdentifierNode - } yield scatterVariable.getText - - val availableValueNames = declarationNames ++ scatterVariableName - + value.getIdentifierNode foreach { identifier => val identifierText = identifier.getText - if (!availableValueNames.contains(identifierText)) { + val declarationNames = value.findDeclarationsAvailableInScope.flatMap(_.declaredValueName) + + if (!declarationNames.contains(identifierText)) { annotationHolder.createErrorAnnotation(identifier.getTextRange, s"No declaration found for '${identifier.getText}'") } } diff --git a/src/winstanley/psi/WdlNamedElement.scala b/src/winstanley/psi/WdlNamedElement.scala new file mode 100644 index 0000000..f50ee76 --- /dev/null +++ b/src/winstanley/psi/WdlNamedElement.scala @@ -0,0 +1,7 @@ +package winstanley.psi + +import com.intellij.psi.PsiNameIdentifierOwner + +trait WdlNamedElement extends PsiNameIdentifierOwner { + def declaredValueName: Option[String] +} diff --git a/src/winstanley/psi/impl/WdlNamedElementImpl.scala b/src/winstanley/psi/impl/WdlNamedElementImpl.scala new file mode 100644 index 0000000..e101bb7 --- /dev/null +++ b/src/winstanley/psi/impl/WdlNamedElementImpl.scala @@ -0,0 +1,11 @@ +package winstanley.psi.impl + +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import winstanley.structure.WdlImplicits._ +import winstanley.psi.WdlNamedElement + +abstract class WdlNamedElementImpl(astNode: ASTNode) extends ASTWrapperPsiElement(astNode) with WdlNamedElement { + // The Option-ality is a little paranoid, but if the declaration is being edited it might be temporarily nameless: + override def declaredValueName: Option[String] = astNode.getPsi.getIdentifierNode.map(_.getText) +} diff --git a/src/winstanley/psi/impl/WdlPsiImplUtil.scala b/src/winstanley/psi/impl/WdlPsiImplUtil.scala new file mode 100644 index 0000000..f1dee45 --- /dev/null +++ b/src/winstanley/psi/impl/WdlPsiImplUtil.scala @@ -0,0 +1,34 @@ +package winstanley.psi.impl + +import com.intellij.psi.{PsiElement, PsiReference} +import winstanley.psi.{WdlVariableLookup, WdlWorkflowBlock} +import winstanley.references.WdlDeclarationReference +import winstanley.structure.WdlImplicits._ + + +/** + * This class is used by the .bnf compiler to implement the 'methods=[...]' methods on PsiElements. + * + * Put all your other junk util methods somewhere else! + */ +object WdlPsiImplUtil extends + WdlNamedElementImplUtil with + WdlVariableLookupImplUtil + +/** + * Provides the getName, setName and getNameIdentifier methods for the WdlNamedElement subclasses (see (eg) declaration and scatter_declaration in the bnf) + */ +sealed trait WdlNamedElementImplUtil { + def getName(namedElement: WdlNamedElementImpl): String = namedElement.declaredValueName.orNull + // TODO: Implement for "refactor/rename" functionality + def setName(namedElement: WdlNamedElementImpl, newName: String): PsiElement = ??? + def getNameIdentifier(namedElement: WdlNamedElementImpl): PsiElement = namedElement.getIdentifierNode.map(_.getPsi).orNull +} + +sealed trait WdlVariableLookupImplUtil { + /** + * This is the method that enables the 'go to declaration' functionality for variable usages. + */ + def getReferences(wdlVariableLookup: WdlVariableLookup): Array[PsiReference] = Array(WdlDeclarationReference(wdlVariableLookup)) +} + diff --git a/src/winstanley/references/WdlDeclarationReference.scala b/src/winstanley/references/WdlDeclarationReference.scala new file mode 100644 index 0000000..be1a7a4 --- /dev/null +++ b/src/winstanley/references/WdlDeclarationReference.scala @@ -0,0 +1,41 @@ +package winstanley.references + +import javax.annotation.Nullable + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.{PsiElement, PsiReferenceBase} +import winstanley.psi.WdlVariableLookup +import winstanley.structure.WdlImplicits._ + +final case class WdlDeclarationReference(value: WdlVariableLookup) extends PsiReferenceBase[PsiElement](value, value.getTextRange){ + + /** + * Returns the element which is the target of the reference !!! OR NULL IF NOT FOUND !!! + * + * @return the target element, or null if it was not possible to resolve the reference to a valid target. + * @see PsiPolyVariantReference#multiResolve(boolean) + */ + @Nullable + override def resolve(): PsiElement = { + value.findDeclarationsAvailableInScope.find(d => value.getIdentifierNode.exists(_.getText == d.getNameIdentifier.getText)).map(_.getNameIdentifier).orNull + } + + /** + * Returns the array of String, PsiElement and/or LookupElement + * instances representing all identifiers that are visible at the location of the reference. The contents + * of the returned array is used to build the lookup list for basic code completion. (The list + * of visible identifiers may not be filtered by the completion prefix string - the + * filtering is performed later by IDEA core.) + * + * @return the array of available identifiers. + */ + override def getVariants: Array[AnyRef] = Array.empty[AnyRef] + + /** + * This override is required to make reference-lookup work. + * + * It needs a relative range within the PsiElement 'value' to count as the reference, + * which in this case is the entire 'WdlVariableLookup' element. + */ + override def getRangeInElement: TextRange = new TextRange(0, value.getTextLength - 1) +} diff --git a/src/winstanley/structure/WdlImplicits.scala b/src/winstanley/structure/WdlImplicits.scala index f34869e..60100a0 100644 --- a/src/winstanley/structure/WdlImplicits.scala +++ b/src/winstanley/structure/WdlImplicits.scala @@ -8,32 +8,28 @@ import winstanley.psi._ object WdlImplicits { implicit final class EnhancedPsiElement(val psiElement: PsiElement) extends AnyVal { - def childTaskBlocks: Set[WdlTaskBlock] = (psiElement.getChildren collect { - case t: WdlTaskBlock => t - }).toSet - def childWorkflowBlocks: Set[WdlWorkflowBlock] = (psiElement.getChildren collect { - case w: WdlWorkflowBlock => w - }).toSet + def contentRange: Option[TextRange] = { + def mapContainingContentRange(psiElement: PsiElement): Option[TextRange] = psiElement.getChildren.collectFirst { + case m: WdlMap => interBraceContentRange(m, WdlTypes.LBRACE, WdlTypes.RBRACE) + }.flatten - def contentRange: Option[TextRange] = psiElement match { - case _: WdlTaskBlock | _: WdlWorkflowBlock | _: WdlTaskOutputs | _: WdlWfOutputs | _: WdlCallBlock | _: WdlScatterBlock | _: WdlIfStmt => - interBraceContentRange(psiElement, WdlTypes.LBRACE, WdlTypes.RBRACE) - case wcb: WdlCommandBlock => interBraceContentRange(wcb, WdlTypes.COMMAND_DELIMITER_OPEN, WdlTypes.COMMAND_DELIMITER_CLOSE) - case _: WdlRuntimeBlock | _: WdlParameterMetaBlock => - mapContainingContentRange(psiElement) - case _ => None - } - - private def mapContainingContentRange(psiElement: PsiElement): Option[TextRange] = psiElement.getChildren.collectFirst { - case m: WdlMap => interBraceContentRange(m, WdlTypes.LBRACE, WdlTypes.RBRACE) - }.flatten + def interBraceContentRange(psiElement: PsiElement, ltype: IElementType, rtype: IElementType): Option[TextRange] = { + for { + lbrace <- Option(psiElement.getNode.findChildByType(ltype)) + rbrace <- Option(psiElement.getNode.findChildByType(rtype)) + } yield new TextRange(lbrace.getTextRange.getStartOffset + 1, rbrace.getTextRange.getEndOffset - 1) + } - private def interBraceContentRange(psiElement: PsiElement, ltype: IElementType, rtype: IElementType): Option[TextRange] = { - for { - lbrace <- Option(psiElement.getNode.findChildByType(ltype)) - rbrace <- Option(psiElement.getNode.findChildByType(rtype)) - } yield new TextRange(lbrace.getTextRange.getStartOffset + 1, rbrace.getTextRange.getEndOffset - 1) + psiElement match { + case _: WdlTaskBlock | _: WdlWorkflowBlock | _: WdlTaskOutputs | _: WdlWfOutputs | _: WdlCallBlock | _: WdlScatterBlock | _: WdlIfStmt => + interBraceContentRange(psiElement, WdlTypes.LBRACE, WdlTypes.RBRACE) + case wcb: WdlCommandBlock => + interBraceContentRange(wcb, WdlTypes.COMMAND_DELIMITER_OPEN, WdlTypes.COMMAND_DELIMITER_CLOSE) + case _: WdlRuntimeBlock | _: WdlParameterMetaBlock => + mapContainingContentRange(psiElement) + case _ => None + } } def findContainingScatter: Option[WdlScatterBlock] = { @@ -54,7 +50,7 @@ object WdlImplicits { psiElement.getChildren.toSet flatMap expandChild } - def findDeclarationsAvailableInScope: Set[WdlDeclaration] = { + def findDeclarationsAvailableInScope: Set[WdlNamedElement] = { Option(psiElement.getParent) map { parent => val siblings = parent.getChildren.filterNot(_ eq psiElement) val siblingDeclarations = siblings collect { @@ -64,31 +60,15 @@ object WdlImplicits { case b: WdlWfBodyElement if b.getIfStmt != null => b.getIfStmt.findDeclarationsInInnerScopes } - parent.findDeclarationsAvailableInScope ++ siblingDeclarations.flatten - } getOrElse Set.empty - } - - def getIdentifierNode: Option[ASTNode] = psiElement.getNode.getChildren(null).collectFirst { - case id if id.getElementType == WdlTypes.IDENTIFIER => id - } - } - - implicit final class EnhancedWdlDeclaration(val wdlDeclaration: WdlDeclaration) extends AnyVal { - // The Option-ality is a little paranoid, but if the declaration is being edited it might be temporarily nameless: - def declaredValueName: Option[String] = wdlDeclaration.getIdentifierNode.map(_.getText) - } + val scatterDeclaration = Option(parent) collect { + case b: WdlWfBodyElement if b.getScatterBlock != null => b.getScatterBlock.getScatterDeclaration + } - implicit final class EnhancedWdlValue(val wdlValue: WdlValue) extends AnyVal { - def asIdentifierNode: Option[ASTNode] = { - if (wdlValue.getChildren.isEmpty) wdlValue.getIdentifierNode else None + parent.findDeclarationsAvailableInScope ++ siblingDeclarations.flatten ++ scatterDeclaration + } getOrElse Set.empty } - } - implicit final class EnhancedWdlTaskBlock(val wdlTaskBlock: WdlTaskBlock) extends AnyVal { - def taskName: String = wdlTaskBlock.getNode.findChildByType(winstanley.psi.WdlTypes.TASK_IDENTIFIER_DECL).getText + def getIdentifierNode: Option[ASTNode] = Option(psiElement.getNode.findChildByType(WdlTypes.IDENTIFIER)) } - implicit final class EnhancedWdlWorkflowBlock(val wdlWorkflowBlock: WdlWorkflowBlock) extends AnyVal { - def workflowName: String = wdlWorkflowBlock.getNode.findChildByType(winstanley.psi.WdlTypes.WORKFLOW_IDENTIFIER_DECL).getText - } } diff --git a/src/winstanley/wdl.bnf b/src/winstanley/wdl.bnf index 8334ae7..284f1bc 100644 --- a/src/winstanley/wdl.bnf +++ b/src/winstanley/wdl.bnf @@ -11,6 +11,8 @@ elementTypeHolderClass="winstanley.psi.WdlTypes" elementTypeClass="winstanley.psi.WdlElementType" tokenTypeClass="winstanley.psi.WdlTokenType" + + psiImplUtilClass="winstanley.psi.impl.WdlPsiImplUtil" } // Regenerate from IntelliJ using Grammar-Kit plugin and COMMAND-SHIFT-G @@ -35,7 +37,8 @@ wf_output_wildcard ::= DOT ASTERISK while_loop ::= WHILE LPAREN expression RPAREN LBRACE wf_body_element* RBRACE if_stmt ::= IF LPAREN expression RPAREN LBRACE wf_body_element* RBRACE -scatter_block ::= SCATTER LPAREN IDENTIFIER IN expression RPAREN LBRACE wf_body_element* RBRACE +scatter_declaration ::= SCATTER LPAREN IDENTIFIER IN expression RPAREN {mixin="winstanley.psi.impl.WdlNamedElementImpl" implements="winstanley.psi.WdlNamedElement" methods=[getName getNameIdentifier setName]} +scatter_block ::= scatter_declaration LBRACE wf_body_element* RBRACE task_block ::= TASK TASK_IDENTIFIER_DECL LBRACE declaration* sections* RBRACE sections ::= command_block|task_outputs|runtime_block|parameter_meta_block|meta_block @@ -55,7 +58,7 @@ runtime_block ::= RUNTIME map parameter_meta_block ::= PARAMETER_META map meta_block ::= META map -declaration ::= type_e IDENTIFIER setter? +declaration ::= type_e IDENTIFIER setter? {mixin="winstanley.psi.impl.WdlNamedElementImpl" implements="winstanley.psi.WdlNamedElement" methods=[getName getNameIdentifier setName]} setter ::= EQUAL expression map ::= LBRACE kv* RBRACE @@ -84,7 +87,8 @@ array_literal ::= LSQUARE expression (COMMA expression)* RSQUARE map_kv ::= expression COLON expression object_kv ::= IDENTIFIER COLON expression -value ::= string_literal | IDENTIFIER | BOOLEAN | float_value | integer_value +value ::= string_literal | variable_lookup | BOOLEAN | float_value | integer_value +variable_lookup ::= IDENTIFIER {methods=[getReferences]} float_value ::= NUMBER+ DOT NUMBER+ integer_value ::= NUMBER+