Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More inspections #22

Merged
merged 18 commits into from
Dec 17, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/grammars/LalrpopParser.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,14 @@ expr_symbol ::= symbol* {
mixin = "com.mdrobnak.lalrpop.psi.ext.LpExprSymbolMixin"
}

symbol ::= LESSTHAN MUT? ID COLON symbol0 GREATERTHAN
| LESSTHAN symbol0 GREATERTHAN
symbol ::= LESSTHAN symbol_name? symbol0 GREATERTHAN
| symbol0 {
mixin = "com.mdrobnak.lalrpop.psi.ext.LpSymbolMixin"
implements = "com.mdrobnak.lalrpop.psi.LpResolveType"
}

symbol_name ::= MUT? ID COLON

symbol0 ::= symbol1 repeat_op* {
implements = "com.mdrobnak.lalrpop.psi.LpResolveType"
mixin = "com.mdrobnak.lalrpop.psi.ext.LpSymbol0Mixin"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class LpRustActionCodeInjector : MultiHostInjector {
val nonterminal = context.parentOfType<LpNonterminal>()!!
val alternative = context.parentOfType<LpAlternative>()!!

val inputs = alternative.selected.mapNotNull { (it as LpSymbolImpl).getSelectedType() }
val inputs = alternative.selected.map { (it as LpSymbolImpl).getSelectedType() }
val returnType = nonterminal.resolveType(listOf())

val grammarDecl = PsiTreeUtil.findChildOfType(context.containingFile, LpGrammarDecl::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mdrobnak.lalrpop.inspections

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.mdrobnak.lalrpop.psi.LpNonterminal
import com.mdrobnak.lalrpop.psi.LpVisitor

object CannotInferNonterminalTypeInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : LpVisitor() {
override fun visitNonterminal(nonterminal: LpNonterminal) {
if (nonterminal.typeRef == null &&
nonterminal.alternatives.alternativeList.isNotEmpty() &&
nonterminal.alternatives.alternativeList.all { it.action != null }
) {
holder.registerProblem(
nonterminal,
"Cannot infer type of nonterminal",
) // TODO Quickfix: get from the rust plugin if available.
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.mdrobnak.lalrpop.inspections

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.parentOfType
import com.mdrobnak.lalrpop.psi.LpNonterminal
import com.mdrobnak.lalrpop.psi.LpNonterminalRef
import com.mdrobnak.lalrpop.psi.LpVisitor
import com.mdrobnak.lalrpop.psi.ext.addParam
import com.mdrobnak.lalrpop.psi.ext.arguments
import com.mdrobnak.lalrpop.psi.ext.createNonterminal

object CannotResolveNonterminalReferenceInspection : LocalInspectionTool() {
override fun buildVisitor(
holder: ProblemsHolder,
isOnTheFly: Boolean
): PsiElementVisitor {
return object : LpVisitor() {
override fun visitNonterminalRef(nonterminalRef: LpNonterminalRef) {
val ref = nonterminalRef.reference
if (ref != null && ref.resolve() == null) {
if (nonterminalRef.arguments == null)
holder.registerProblem(
nonterminalRef,
"Cannot resolve ${ref.canonicalText}",
AddToMacroParamsQuickFix,
CreateNonterminalQuickFix
)
else
holder.registerProblem(
nonterminalRef,
"Cannot resolve ${ref.canonicalText}",
CreateNonterminalQuickFix
)
}
}
}
}
}

object AddToMacroParamsQuickFix : LocalQuickFix {
override fun getFamilyName(): String = "Add to macro parameters"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val nonterminal = descriptor.psiElement.parentOfType<LpNonterminal>() ?: return
nonterminal.nonterminalName.addParam(descriptor.psiElement.text)
}
}

object CreateNonterminalQuickFix : LocalQuickFix {
override fun getFamilyName(): String = "Create nonterminal"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
(descriptor.psiElement as LpNonterminalRef).createNonterminal()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.mdrobnak.lalrpop.inspections

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolderBase
Expand Down Expand Up @@ -49,7 +48,6 @@ object MissingTerminalsInspection : LocalInspectionTool() {
problemsHolder.registerProblem(
unresolvedElement,
"Missing declaration of terminal",
ProblemHighlightType.ERROR
)
}
}
Expand All @@ -60,28 +58,38 @@ object MissingTerminalsInspection : LocalInspectionTool() {
isOnTheFly: Boolean,
session: LocalInspectionToolSession
): PsiElementVisitor {
return object : PsiElementVisitor() {
override fun visitElement(element: PsiElement) {
super.visitElement(element)
if (element is LpMatchToken || element is LpEnumToken) session.dataNotNull(checkKey).set(true)
return object : LpVisitor() {
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
override fun visitMatchToken(o: LpMatchToken) {
session.dataNotNull(checkKey).set(true)
}

if (element is LpMatchItem) {
val terminal = element.matchSymbol?.quotedLiteral
if (terminal != null) {
session.dataNotNull(terminalDefsKey).add(terminal)
} else {
// is _ => there cannot be any unresolved terminals, so don't check.
session.dataNotNull(matchHasWildcardKey).set(true)
}
} else if (element is LpTerminal && element.parent is LpConversion) {
override fun visitEnumToken(o: LpEnumToken) {
session.dataNotNull(checkKey).set(true)
}

override fun visitMatchItem(element: LpMatchItem) {
val terminal = element.matchSymbol?.quotedLiteral
if (terminal != null) {
session.dataNotNull(terminalDefsKey).add(terminal)
} else {
// is _ => there cannot be any unresolved terminals, so don't check.
session.dataNotNull(matchHasWildcardKey).set(true)
}
}

override fun visitTerminal(element: LpTerminal) {
if (element.parent is LpConversion) {
val terminal = element.quotedTerminal
if (terminal != null) {
session.dataNotNull(terminalDefsKey).add(terminal)
}
} else if (element is LpQuotedTerminal && element.parent !is LpTerminal) {
session.dataNotNull(unresolvedKey).add(element)
}
}

override fun visitQuotedTerminal(element: LpQuotedTerminal) {
if (element.parent !is LpTerminal)
session.dataNotNull(unresolvedKey).add(element)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.mdrobnak.lalrpop.inspections

import com.intellij.codeInspection.*
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import com.mdrobnak.lalrpop.psi.*
import com.mdrobnak.lalrpop.psi.ext.isExplicitlySelected
import com.mdrobnak.lalrpop.psi.ext.isNamed
import com.mdrobnak.lalrpop.psi.ext.removeName

/**
* Inspection related to where named symbols can appear.
* Suggests removing the name if there are issues.
*/
object NamedSymbolsInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : LpVisitor() {
override fun visitSymbol(symbol: LpSymbol) {
if (symbol.isNamed) {
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
val parent = PsiTreeUtil.findFirstParent(symbol) {
it is LpAlternative || it is LpExprSymbol || it is LpNonterminalArguments || it is LpTypeOfSymbol
}
if (parent is LpAlternative) {
// if there is at least one explicitly selected, unnamed symbol
if (parent.symbolList.any { it.isExplicitlySelected && !it.isNamed }) {
// then it is an error
holder.registerProblem(
symbol,
"Usage of named symbol in an alternative where an unnamed symbol was also used",
RemoveNameQuickFix
)
}
} else {
holder.registerProblem(
symbol,
"Usage of named symbol in a context that doesn't allow it",
RemoveNameQuickFix
)
}
}
}
}
}
}

object RemoveNameQuickFix : LocalQuickFix {
override fun getFamilyName(): String {
return "Remove name"
}

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
(descriptor.psiElement as LpSymbol).removeName()
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/mdrobnak/lalrpop/psi/LpElementFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.mdrobnak.lalrpop.LpLanguage
import org.rust.lang.core.psi.ext.childrenWithLeaves
import org.rust.lang.core.psi.ext.descendantOfTypeStrict
import org.rust.lang.core.psi.ext.elementType

class LpElementFactory(val project: Project) {
fun createPsiFile(text: CharSequence): PsiFile =
Expand All @@ -19,4 +21,22 @@ class LpElementFactory(val project: Project) {
fun createIdentifier(name: String): PsiElement =
createFromText<LpNonterminalName>("grammar;\n$name = \" \";")?.nameIdentifier
?: error("Failed to create identifier: `$name`")

fun createNonterminalParamsFromSingle(name: String): PsiElement =
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
createFromText<LpNonterminalParams>("grammar;\ndummy<$name> = {};")
?: error("Failed to create nonterminal params from single param with name = `$name`")

fun createComma(): PsiElement =
createFromText<LpNonterminalParams>("grammar;\ndummy<T,U> = {};")?.childrenWithLeaves?.first { it.elementType == LpElementTypes.COMMA }
?: error("Failed to create psi element for comma (`,`)")

fun createNonterminalParam(name: String): PsiElement =
createFromText<LpNonterminalParam>("grammar;\ndummy<$name> = {};")
?: error("Failed to create nonterminal param from from name = `$name`")

fun createNonterminal(name: String, params: List<String>?): PsiElement {
val paramsString = params?.joinToString(prefix = "<", separator = ", ", postfix = ">") ?: ""
return createFromText<LpNonterminal>("grammar;\n$name$paramsString = ();")
?: error("Failed to create nonterminal with name = `$name` and params = `$params`")
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/com/mdrobnak/lalrpop/psi/ext/LpNonterminal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import com.intellij.lang.ASTNode
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.mdrobnak.lalrpop.psi.LpGrammarItem
import com.mdrobnak.lalrpop.psi.LpNonterminal
import com.mdrobnak.lalrpop.psi.NonterminalGenericArgument

val LpNonterminal.grammarItemParent: LpGrammarItem
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
get() = this.parent as LpGrammarItem

abstract class LpNonterminalMixin(node: ASTNode) : ASTWrapperPsiElement(node), LpNonterminal {
override fun resolveType(arguments: List<NonterminalGenericArgument>): String =
if (this.nonterminalName.nonterminalParams != null) {
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/com/mdrobnak/lalrpop/psi/ext/LpNonterminalName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ import com.mdrobnak.lalrpop.psi.LpElementFactory
import com.mdrobnak.lalrpop.psi.LpElementTypes
import com.mdrobnak.lalrpop.psi.LpNonterminal
import com.mdrobnak.lalrpop.psi.LpNonterminalName
import org.rust.lang.core.psi.ext.childrenWithLeaves

val LpNonterminalName.nonterminalParent: LpNonterminal
get() = this.parent as LpNonterminal

fun LpNonterminalName.addParam(name: String) {
val factory = LpElementFactory(project)
val params = this.nonterminalParams
if (params == null) {
this.add(factory.createNonterminalParamsFromSingle(name))
} else {
val comma = factory.createComma()
val newParam = factory.createNonterminalParam(name)
val lastParam = params.nonterminalParamList.lastOrNull()
if (lastParam != null) {
val insertedComma = params.addAfter(comma, lastParam)
params.addAfter(newParam, insertedComma)
} else {
// add after the `<`
params.addAfter(newParam, params.childrenWithLeaves.first())
}
}
}

abstract class LpNonterminalNameMixin(node: ASTNode) : ASTWrapperPsiElement(node), LpNonterminalName {
override fun getNameIdentifier(): PsiElement {
return node.findChildByType(LpElementTypes.ID)!!.psi
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/com/mdrobnak/lalrpop/psi/ext/LpNonterminalRef.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@ package com.mdrobnak.lalrpop.psi.ext
import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiReference
import com.intellij.psi.util.parentOfType
import com.mdrobnak.lalrpop.psi.*
import com.mdrobnak.lalrpop.resolve.LpNonterminalReference

val LpNonterminalRef.arguments: LpNonterminalArguments?
get() = this.nextSibling as? LpNonterminalArguments

fun LpNonterminalRef.createNonterminal() {
val factory = LpElementFactory(project)

val nonterminal = this.parentOfType<LpNonterminal>() ?: return
val grammarItem = nonterminal.parentOfType<LpGrammarItem>() ?: return
AzureMarker marked this conversation as resolved.
Show resolved Hide resolved
val grammar = grammarItem.parent

grammar.addAfter(
factory.createNonterminal(this.text, this.arguments?.symbolList?.mapIndexed { index, _ -> "Rule${index+1}" }),
grammarItem,
)
}

abstract class LpNonterminalRefMixin(node: ASTNode) : ASTWrapperPsiElement(node), LpNonterminalRef {
override fun getReference(): PsiReference {
return LpNonterminalReference(this)
Expand Down
19 changes: 16 additions & 3 deletions src/main/kotlin/com/mdrobnak/lalrpop/psi/ext/LpSymbol.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,23 @@ import org.rust.lang.core.psi.ext.elementType
val LpSymbol.isExplicitlySelected: Boolean
get() = this.childrenWithLeaves.first().elementType == LpElementTypes.LESSTHAN

val LpSymbol.isNamed: Boolean
get() = this.symbolName != null

val LpSymbol.isMutable: Boolean
get() = this.symbolName?.childrenWithLeaves?.any { it.elementType == LpElementTypes.MUT } ?: false

val LpSymbol.symbolNameString: String?
get() = this.symbolName?.childrenWithLeaves?.find { it.elementType == LpElementTypes.ID }?.text

fun LpSymbol.removeName() {
this.symbolName?.delete()
}

abstract class LpSymbolMixin(node: ASTNode) : ASTWrapperPsiElement(node), LpSymbol {
fun getSelectedType(): LpSelectedType? {
val isMutable = childrenWithLeaves.any { it.elementType == LpElementTypes.MUT }
val name = childrenWithLeaves.find { it.elementType == LpElementTypes.ID }?.text
fun getSelectedType(): LpSelectedType {
val isMutable = this.isMutable
val name = this.symbolNameString
val type = this.resolveType(listOf())

return if (name != null) {
Expand Down
Loading