[Grazie] Add multiline comments grammar check support for YAML
authorPavel Bakhvalov <pavel.bakhvalov@jetbrains.com>
Wed, 12 Aug 2020 13:32:09 +0000 (16:32 +0300)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Wed, 12 Aug 2020 18:21:16 +0000 (18:21 +0000)
GitOrigin-RevId: ca47c35ae308e5099514c967191688de880db10e

plugins/grazie/src/main/kotlin/com/intellij/grazie/grammar/ide/GraziePsiElementProcessor.kt
plugins/grazie/src/main/kotlin/com/intellij/grazie/grammar/strategy/GrammarCheckingStrategy.kt
plugins/grazie/src/main/kotlin/com/intellij/grazie/grammar/strategy/StrategyUtils.kt
plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/inspection/grammar/GrazieInspection.kt
plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/language/java/JavaGrammarCheckingStrategy.kt
plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/language/yaml/YamlGrammarCheckingStrategy.kt
plugins/grazie/src/test/kotlin/com/intellij/grazie/GrazieTestBase.kt
plugins/grazie/src/test/kotlin/com/intellij/grazie/grammar/GrammarCheckerTests.kt
plugins/grazie/src/test/testData/ide/language/yaml/Example.yaml

index 903c29adbb4d29884d700916d74051dff34b26aa..4f8bc6c4ae107b180ea2cd630620dd026902bf6e 100644 (file)
@@ -9,7 +9,6 @@ import com.intellij.grazie.grammar.strategy.impl.ReplaceCharRule
 import com.intellij.grazie.grammar.strategy.impl.RuleGroup
 import com.intellij.grazie.utils.processElements
 import com.intellij.psi.PsiElement
-import com.intellij.psi.TokenType.WHITE_SPACE
 import com.intellij.psi.search.PsiElementProcessor
 import com.intellij.psi.util.PsiTreeUtil
 import com.intellij.psi.util.elementType
@@ -31,20 +30,29 @@ internal class GraziePsiElementProcessor<T : PsiElement>(
       val parent = PsiTreeUtil.findCommonParent(roots)
       require(parent != null) { "Chained roots must have a common parent" }
       val processor = GraziePsiElementProcessor<PsiElement>(parent, strategy)
+      val whitespaceTokens = strategy.getWhiteSpaceTokens()
 
       with (processor) {
         var prevOffset = parent.startOffset
+        var isWhitespaceNeeded = false
         for (root in roots) {
-          if (root.elementType == WHITE_SPACE) {
+          if (root.elementType in whitespaceTokens) {
             shifts.add(ElementShift(cumulativeTextLength, root.endOffset - prevOffset))
+            isWhitespaceNeeded = true
+            prevOffset = root.endOffset
+            continue
+          }
+
+          require(strategy.isMyContextRoot(root)) { "PsiElement must be a context root or be in whitespaceTokens of strategy" }
+
+          if (isWhitespaceNeeded) {
+            isWhitespaceNeeded = false
             text.lastOrNull()?.let {
               val index = shifts.size - 1
               cumulativeTextLength += 1
-              shifts[index] = ElementShift(cumulativeTextLength, (root.endOffset - prevOffset) - 1)
+              shifts[index] = ElementShift(cumulativeTextLength, shifts[index].length - 1)
               it.append(' ')
             }
-            prevOffset = root.endOffset
-            continue
           }
 
           text.add(StringBuilder())
@@ -58,7 +66,7 @@ internal class GraziePsiElementProcessor<T : PsiElement>(
         }
       }
 
-      val rootsWithText = roots.filter { it.elementType != WHITE_SPACE }.zip(processor.text) { root, text -> RootWithText(root, text) }
+      val rootsWithText = roots.filter { it.elementType !in whitespaceTokens }.zip(processor.text) { root, text -> RootWithText(root, text) }
       return Result(parent, processor.tokens, processor.shifts, rootsWithText)
     }
   }
index 308cf070cb0285cbf7f56b328bf288a1caf8a28f..913a6f8adc384a7909b95fc1ced3e22b060e84f8 100644 (file)
@@ -11,9 +11,11 @@ import com.intellij.grazie.grammar.strategy.impl.RuleGroup
 import com.intellij.grazie.utils.LinkedSet
 import com.intellij.grazie.utils.orTrue
 import com.intellij.lang.Language
+import com.intellij.lang.LanguageParserDefinitions
+import com.intellij.lang.ParserDefinition
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.psi.PsiElement
-import com.intellij.psi.TokenType.WHITE_SPACE
+import com.intellij.psi.tree.TokenSet
 import org.jetbrains.annotations.ApiStatus
 
 /**
@@ -106,16 +108,29 @@ interface GrammarCheckingStrategy {
   fun isMyContextRoot(element: PsiElement): Boolean
 
   /**
+   * Determine tokens which should be treated as whitespaces for the current [Language].
+   * Default implementation considers that these tokens are the same as [ParserDefinition.getWhitespaceTokens].
+   *
+   * @return [TokenSet] of whitespace tokens
+   */
+  @JvmDefault
+  fun getWhiteSpaceTokens(): TokenSet {
+    val extension = StrategyUtils.getStrategyExtensionPoint(this)
+    val language = Language.findLanguageByID(extension.language) ?: return TokenSet.WHITE_SPACE
+    return LanguageParserDefinitions.INSTANCE.forLanguage(language).whitespaceTokens
+  }
+
+  /**
    * Determine PsiElement roots that should be considered as a continuous text including [root].
    * [root] element MUST be present in chain.
    * Passing any sub-element in chain must return the same list of all the elements in the chain.
    * Chain roots must be in the same [TextDomain] and have the same [GrammarCheckingStrategy]
-   * or be a [WHITE_SPACE].
+   * or be one of [getWhiteSpaceTokens].
    * For example, this method can be used to combine single-line comments into
    * a single block of text for grammar check.
    *
    * @param root root element previously selected in [isMyContextRoot]
-   * @return list of root elements that should be considered as a continuous text with [WHITE_SPACE] elements
+   * @return list of root elements that should be considered as a continuous text with [getWhiteSpaceTokens] elements
    */
   @JvmDefault
   fun getRootsChain(root: PsiElement): List<PsiElement> = listOf(root)
index 5ede3cf9412ccd21035f0257035403092f791d7e..8c7ece7265353586f80dd3cccdb99ecfd2311bf1 100644 (file)
@@ -25,7 +25,7 @@ object StrategyUtils {
    * @return extension point
    */
   internal fun getStrategyExtensionPoint(strategy: GrammarCheckingStrategy): LanguageExtensionPoint<GrammarCheckingStrategy> {
-    return LanguageGrammarChecking.getExtensionPointByStrategy(strategy) ?: error("${strategy.getName()} strategy is not registered")
+    return LanguageGrammarChecking.getExtensionPointByStrategy(strategy) ?: error("Strategy is not registered")
   }
 
   internal fun getTextDomainOrDefault(root: PsiElement, default: TextDomain): TextDomain {
@@ -112,9 +112,9 @@ object StrategyUtils {
    * @param types possible types of siblings
    * @return sequence of siblings with whitespace tokens
    */
-  fun getNotSoDistantSiblingsOfTypes(element: PsiElement, types: Set<IElementType>) = sequence {
+  fun getNotSoDistantSiblingsOfTypes(strategy: GrammarCheckingStrategy, element: PsiElement, types: Set<IElementType>) = sequence {
     fun PsiElement.process(types: Set<IElementType>, next: Boolean) = sequence<PsiElement> {
-      val parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(language)
+      val whitespaceTokens = strategy.getWhiteSpaceTokens()
       var newLinesBetweenSiblingsCount = 0
 
       var sibling: PsiElement? = this@process
@@ -125,7 +125,7 @@ object StrategyUtils {
             newLinesBetweenSiblingsCount = 0
             candidate
           }
-          in parserDefinition.whitespaceTokens -> {
+          in whitespaceTokens -> {
             newLinesBetweenSiblingsCount += candidate.text.count { char -> char == '\n' }
             if (newLinesBetweenSiblingsCount > 1) null else candidate
           }
index e999fda8d03df3eaddc4d5a8584ebb3fff02c864..0d6f2bc2a5f4ed1b0a4450ba104bac070b6891ab 100644 (file)
@@ -16,7 +16,6 @@ import com.intellij.grazie.ide.inspection.grammar.problem.GrazieProblemDescripto
 import com.intellij.grazie.ide.language.LanguageGrammarChecking
 import com.intellij.grazie.ide.msg.GrazieStateLifecycle
 import com.intellij.grazie.utils.lazyConfig
-import com.intellij.lang.LanguageParserDefinitions
 import com.intellij.lang.injection.InjectedLanguageManager
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.project.ProjectManager
@@ -81,8 +80,8 @@ class GrazieInspection : LocalInspectionTool() {
             require(roots.isNotEmpty()) { "Roots chain MUST contain at least one element (self)" }
 
             if (!checkIfAlreadyProcessed(roots.first(), session)) {
-              val parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(element.language)
-              val rootsWithoutWhitespaces = roots.filter { !parserDefinition.whitespaceTokens.contains(it.elementType) }
+              val whitespaceTokens = strategy.getWhiteSpaceTokens()
+              val rootsWithoutWhitespaces = roots.filter { it.elementType !in whitespaceTokens }
 
               require(rootsWithoutWhitespaces.all {
                 strategy in LanguageGrammarChecking.getStrategiesForElement(element, enabledStrategiesIDs, disabledStrategiesIDs)
index 4a5921c06ae2335a4d5f26618af6b4f377d307cb..5d9dc43a5d5d00dfaff01bb9f3977a70d847bb06 100644 (file)
@@ -50,7 +50,7 @@ class JavaGrammarCheckingStrategy : BaseGrammarCheckingStrategy {
   private val SINGLE_LINE_COMMENT_TYPES = setOf(END_OF_LINE_COMMENT, C_STYLE_COMMENT)
   override fun getRootsChain(root: PsiElement): List<PsiElement> {
     return if (root.elementType in SINGLE_LINE_COMMENT_TYPES) {
-      StrategyUtils.getNotSoDistantSiblingsOfTypes(root, SINGLE_LINE_COMMENT_TYPES).toList()
+      StrategyUtils.getNotSoDistantSiblingsOfTypes(this, root, SINGLE_LINE_COMMENT_TYPES).toList()
     }
     else super.getRootsChain(root)
   }
index e7fe51146b8f61b8701dcb6a2dd2c9da76638d22..44072b761df26dffcc507a2cec3cd9e39ac73647 100644 (file)
@@ -6,6 +6,8 @@ import com.intellij.grazie.grammar.strategy.GrammarCheckingStrategy.TextDomain
 import com.intellij.grazie.grammar.strategy.StrategyUtils
 import com.intellij.psi.PsiElement
 import com.intellij.psi.impl.source.tree.PsiCommentImpl
+import com.intellij.psi.tree.TokenSet
+import com.intellij.psi.util.elementType
 import org.jetbrains.yaml.YAMLTokenTypes.*
 
 class YamlGrammarCheckingStrategy : BaseGrammarCheckingStrategy {
@@ -30,4 +32,13 @@ class YamlGrammarCheckingStrategy : BaseGrammarCheckingStrategy {
     is PsiCommentImpl -> StrategyUtils.indentIndexes(text, setOf(' ', '\t', '#'))
     else -> StrategyUtils.emptyLinkedSet()
   }
+
+  override fun getWhiteSpaceTokens() = TokenSet.create(WHITESPACE, INDENT, EOL)
+
+  override fun getRootsChain(root: PsiElement): List<PsiElement> {
+    return if (root.elementType == COMMENT) {
+      StrategyUtils.getNotSoDistantSiblingsOfTypes(this, root, setOf(COMMENT)).toList()
+    }
+    else super.getRootsChain(root)
+  }
 }
index c633c5998b41274ff0ddea746f07931419413b64..9bef505813e6d732fcef4d6c70a497959d91bc4a 100644 (file)
@@ -1,7 +1,11 @@
 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
 package com.intellij.grazie
 
+import com.intellij.grazie.grammar.GrammarChecker
+import com.intellij.grazie.grammar.Typo
+import com.intellij.grazie.grammar.check
 import com.intellij.grazie.ide.inspection.grammar.GrazieInspection
+import com.intellij.grazie.ide.language.LanguageGrammarChecking
 import com.intellij.grazie.jlanguage.Lang
 import com.intellij.grazie.utils.filterFor
 import com.intellij.psi.PsiElement
@@ -42,4 +46,10 @@ abstract class GrazieTestBase : BasePlatformTestCase() {
   fun plain(texts: List<String>): Collection<PsiElement> {
     return texts.flatMap { myFixture.configureByText("${it.hashCode()}.txt", it).filterFor<PsiPlainText>() }
   }
+
+  fun check(tokens: Collection<PsiElement>): Set<Typo> {
+    if (tokens.isEmpty()) return emptySet()
+    val strategy = LanguageGrammarChecking.allForLanguage(tokens.first().language).first()
+    return GrammarChecker.check(tokens, strategy)
+  }
 }
index 4862181f5ab6c50f1e2c7030afd50faebb5425ed..6dfa0fd05a39d9331ef8f19ca1fb88e905c55626 100644 (file)
@@ -2,7 +2,6 @@
 package com.intellij.grazie.grammar
 
 import com.intellij.grazie.GrazieTestBase
-import com.intellij.grazie.ide.language.plain.PlainTextGrammarCheckingStrategy
 import org.junit.Assert
 import org.junit.Test
 
@@ -11,21 +10,21 @@ class GrammarCheckerTests : GrazieTestBase() {
   @Test
   fun `test empty text`() {
     val token = plain("")
-    val fixes = GrammarChecker.check(token, PlainTextGrammarCheckingStrategy())
+    val fixes = check(token)
     assertIsEmpty(fixes)
   }
 
   @Test
   fun `test correct text`() {
     val token = plain("Hello world")
-    val fixes = GrammarChecker.check(token, PlainTextGrammarCheckingStrategy())
+    val fixes = check(token)
     assertIsEmpty(fixes)
   }
 
   @Test
   fun `test few lines of correct text`() {
     val tokens = plain("Hello world!\n", "This is the start of a message.\n", "The end is also here.")
-    val fixes = GrammarChecker.check(tokens, PlainTextGrammarCheckingStrategy())
+    val fixes = check(tokens)
     assertIsEmpty(fixes)
   }
 
@@ -34,7 +33,7 @@ class GrammarCheckerTests : GrazieTestBase() {
   fun `test one line of text with one typo`() {
     val text = "Tot he world, my dear friend"
     val tokens = plain(text).toList()
-    val fixes = GrammarChecker.check(tokens, PlainTextGrammarCheckingStrategy())
+    val fixes = check(tokens)
     fixes.single().assertTypoIs(IntRange(0, 5), listOf("To the"), text)
   }
 
@@ -42,7 +41,7 @@ class GrammarCheckerTests : GrazieTestBase() {
   fun `test few lines of text with typo on first line`() {
     val text = listOf("Tot he world, my dear friend!\n", "This is the start of a message.\n", "The end is also here world\n")
     val tokens = plain(text)
-    val fixes = GrammarChecker.check(tokens, PlainTextGrammarCheckingStrategy())
+    val fixes = check(tokens)
     fixes.single().assertTypoIs(IntRange(0, 5), listOf("To the"), text[0])
   }
 
@@ -50,7 +49,7 @@ class GrammarCheckerTests : GrazieTestBase() {
   fun `test few lines of text with typo on last line`() {
     val text = listOf("Hello world!\n", "This is the start of a message.\n", "It is a the friend\n")
     val tokens = plain(text)
-    val fixes = GrammarChecker.check(tokens, PlainTextGrammarCheckingStrategy())
+    val fixes = check(tokens)
     fixes.single().assertTypoIs(IntRange(6, 10), listOf("a", "the"), text[2])
   }
 
@@ -58,7 +57,7 @@ class GrammarCheckerTests : GrazieTestBase() {
   fun `test few lines of text with few typos`() {
     val text = listOf("Hello. World,, tot he.\n", "This are my friend.")
     val tokens = plain(text)
-    val fixes = GrammarChecker.check(tokens, PlainTextWithoutSpacesGrammarCheckingStrategy()).toList()
+    val fixes = check(tokens).toList()
     Assert.assertEquals(3, fixes.size)
     fixes[0].assertTypoIs(IntRange(12, 13), listOf(","), text[0])
     fixes[1].assertTypoIs(IntRange(15, 20), listOf("to the"), text[0])
@@ -70,7 +69,7 @@ class GrammarCheckerTests : GrazieTestBase() {
     val text = listOf("English text.  Hello. World,, tot he.  \n  ", "     This is the next Javadoc string.   \n",
                       "    This are my friend.    ")
     val tokens = plain(text)
-    val fixes = GrammarChecker.check(tokens, PlainTextWithoutSpacesGrammarCheckingStrategy()).toList()
+    val fixes = check(tokens).toList()
     Assert.assertEquals(3, fixes.size)
     fixes[0].assertTypoIs(IntRange(27, 28), listOf(","), text[0])
     fixes[1].assertTypoIs(IntRange(30, 35), listOf("to the"), text[0])
index 9106dd25f2ad9d64844b5e38c4a334a601414828..8561f057c7ef94b243e111fdef689a2229ff2b76 100644 (file)
@@ -8,12 +8,17 @@ customer:
 items:
   - part_no:   A4786
     descrip:   Water Bucket (Filled)
+    # This
+
+    # Are good price
     price:     1.47
     quantity:  4
 
   - part_no:   E1628
     'it are error here':   It <warning descr="IT_VBZ">are</warning> error here
     size:      8
+    # It
+    # <warning descr="IT_VBZ">are</warning> bad price
     price:     133.7
     quantity:
       - It <warning descr="IT_VBZ">are</warning> error here