diff --git a/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt b/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt index 8211d7e..6bf3f67 100644 --- a/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt +++ b/src/main/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotator.kt @@ -358,12 +358,23 @@ class DocscribeAnnotator : ExternalAnnotator if (annotationResult != null) { val projectDir = file.project.basePath ?: "" val useRbs = RbsDetector.shouldUseRbs(projectDir) + val warnInvalidYard = + try { + DocscribeSettings.getInstance().warnOnInvalidYardTypes + } catch (_: Exception) { + true + } for (parsedFile in annotationResult.files) { for (offense in parsedFile.offenses) { val isRbsTypeUpdateRaw = offense.copName == "Docscribe/UpdatedParam" || offense.copName == "Docscribe/UpdatedReturn" val isInvalidYardRaw = offense.copName == "Docscribe/InvalidType" val isError = offense.copName == "Docscribe/Error" val isRbsSource = offense.source == "rbs" || offense.message.contains("RBS") + // Warn toggle OFF hides syntax-driven InvalidType; RBS ones stay. + if (isInvalidYardHidden(offense.copName, offense.source, offense.message, warnInvalidYard)) { + log.info("DocScribe apply skipping InvalidType for $filePath (warnOnInvalidYardTypes=false)") + continue + } val baseLine = (offense.location.startLine - 1).coerceIn(0, document.lineCount - 1) // For RBS updates and invalid YARD, highlight the YARD comment, not the def. Errors stay on line 1. val line = @@ -409,8 +420,8 @@ class DocscribeAnnotator : ExternalAnnotator } } } - // YARD syntax validation without RBS is now handled by the gem via --validate-types - // (Yard::Validator + TypeMismatchValidator) and appears as Docscribe/InvalidType above + // Syntax-driven InvalidType is filtered above when the warn setting is + // off (see isInvalidYardHidden); RBS-sourced InvalidType always shows. } private fun showGemNotInstalledBalloon( @@ -552,6 +563,28 @@ class DocscribeAnnotator : ExternalAnnotator nowMs: Long = System.currentTimeMillis(), ): Boolean = lastShownMs == null || nowMs - lastShownMs >= BALLOON_THROTTLE_MS + /** + * Whether a `Docscribe/InvalidType` offense should be hidden because the + * "Warn on invalid YARD types" setting is off. RBS-sourced offenses always + * stay visible — the toggle only gates syntax-driven ones. + * + * @param copName Cop name of the offense. + * @param source `source` field of the offense (`rbs`, `infer`, `syntax`, or `null`). + * @param message Offense message (legacy RBS marker fallback). + * @param warnInvalidYardTypes Current value of the setting. + * @return `true` when the offense must be skipped in [apply]. + */ + @JvmStatic + fun isInvalidYardHidden( + copName: String, + source: String?, + message: String, + warnInvalidYardTypes: Boolean, + ): Boolean { + if (copName != "Docscribe/InvalidType" || warnInvalidYardTypes) return false + return !(source == "rbs" || message.contains("RBS")) + } + /** * Generation counter per file path. * diff --git a/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorWarnToggleTest.kt b/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorWarnToggleTest.kt new file mode 100644 index 0000000..6f07642 --- /dev/null +++ b/src/test/kotlin/com/florexlabs/docscribe/annotator/DocscribeAnnotatorWarnToggleTest.kt @@ -0,0 +1,75 @@ +package com.florexlabs.docscribe.annotator + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DocscribeAnnotatorWarnToggleTest { + @Test + fun `warn on shows syntax invalid type`() { + assertFalse( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/InvalidType", + "syntax", + "invalid YARD type [Symbкol] for @param x", + warnInvalidYardTypes = true, + ), + ) + } + + @Test + fun `warn off hides syntax invalid type`() { + assertTrue( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/InvalidType", + "syntax", + "invalid YARD type [Symbкol] for @param x", + warnInvalidYardTypes = false, + ), + ) + } + + @Test + fun `warn off hides invalid type with null source`() { + assertTrue( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/InvalidType", + null, + "invalid YARD type [Array<] for @return", + warnInvalidYardTypes = false, + ), + ) + } + + @Test + fun `warn off keeps rbs sourced invalid type`() { + assertFalse( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/InvalidType", + "rbs", + "invalid YARD type [String] for @param x", + warnInvalidYardTypes = false, + ), + ) + } + + @Test + fun `warn off keeps other cops`() { + assertFalse( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/MissingDocBlock", + "infer", + "missing docs for Calc#add", + warnInvalidYardTypes = false, + ), + ) + assertFalse( + DocscribeAnnotator.isInvalidYardHidden( + "Docscribe/UpdatedParam", + "rbs", + "updated @param x from String to Integer", + warnInvalidYardTypes = false, + ), + ) + } +} diff --git a/tools/gui-qa/cases/3h5.sh b/tools/gui-qa/cases/3h5.sh index ca9d69d..0e5c5b1 100755 --- a/tools/gui-qa/cases/3h5.sh +++ b/tools/gui-qa/cases/3h5.sh @@ -1,13 +1,12 @@ #!/bin/zsh # 3H.5 Warn toggle live: bad.rb with [Symbкol] (CYRILLIC к, U+043A) and NO # RBS (sig/ moved away). Settings ON -> annotator flags (offenses=1) + -# intention fix row without restart. OFF leg: DRIFT (proven 2026-09-18) — -# the gem ALWAYS emits invalid_type for syntax-broken YARD -# (handle_existing_param -> invalid_yard_type? is unconditional; the -# --validate-types flag only gates *mismatch-vs-inferred* reporting, and -# the plugin's toggle is not even wired into RunOptions for CHECK). So OFF -# still yields offenses=1. Assert ON (count + fix row); record OFF=1 as -# drift in the note. +# intention fix row without restart. OFF -> plugin filters the +# syntax-driven InvalidType in apply() (log: "skipping InvalidType ..."), +# RBS-sourced ones would stay. The gem still emits the offense (its +# --validate-types flag only gates mismatch-vs-inferred reporting), so the +# "apply ... offenses=1" line is pre-filter and CANNOT be the OFF oracle — +# assert the skip line instead. cd "$(dirname "$0")/.." || exit 2 source ./lib.sh || exit 2 source ./menu.sh || exit 2 @@ -89,8 +88,10 @@ M1="$(log_mark)" rewrite_run '~/qa-stand/bad.rb' sleep 20 A2="$(gssh "awk 'NR>$M1' ~/Library/Logs/JetBrains/RubyMine2026.2/idea.log" | grep -F 'DocScribe apply file=/Users/admin/qa-stand/bad.rb' | tail -n 1)" -print -r -- "OFF apply (drift, still flagged): [$A2]" >&2 +SKIP2="$(gssh "awk 'NR>$M1' ~/Library/Logs/JetBrains/RubyMine2026.2/idea.log" | grep -F 'skipping InvalidType for /Users/admin/qa-stand/bad.rb' | tail -n 1)" +[[ -n "$SKIP2" ]] || RC=2 +print -r -- "OFF skip line: [$SKIP2]" >&2 cleanup if [[ $RC -eq 1 ]]; then fail "3h5" "no highlight when ON"; exit 1; fi -fail "3h5" "OFF still flags InvalidType (gem always emits; toggle unwired for CHECK)" -exit 1 +if [[ $RC -eq 2 ]]; then echo "--- log tail ---" >&2; echo "$A2" >&2; fail "3h5" "no skip line when OFF"; exit 1; fi +pass "3h5"