From c84f70939b26acd33ccfe17162a570887ef7b8ae Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sun, 30 Aug 2026 12:22:45 -0700 Subject: [PATCH] Demote detached_docstring to a warning in default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule shipped at `:error` in `default`, which makes `julialint` exit non-zero — so upgrading the tooling would fail CI for any project with a single detached docstring, the exact harm the new-rules-enter-off convention exists to prevent. The detector is also a heuristic (a signature-pattern filter), while the only other default-`:error` rules are definitional breakage detectors that cannot be wrong, and `include_errors` — arguably worse breakage — sits at `:warning`. Default-on stays: a severed docstring discards its text outright rather than expressing a style preference. But the carve-out now has a stated cap, in the preset docs and the registry comment both: a rule of this class enters `default` at `:warning` at most, never `:error`, so an upgrade can never change `julialint`'s exit status. Co-Authored-By: Claude Fable 5 --- docs/src/configuration.md | 6 +++++- src/lint_rules.jl | 11 ++++++++--- test/test_config.jl | 2 +- test/test_lint_syntax_rules.jl | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/src/configuration.md b/docs/src/configuration.md index af6633d1..1bd875e3 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -266,7 +266,7 @@ rules run on the JuliaSyntax tree of a single file alone | `detached_docstring` | A string that looks like a docstring but is not attached to anything, because a comment or a blank line sits between it and the expression it documents. The text is evaluated and discarded. | All of these are `"off"` outside the `strict` preset, except `detached_docstring`, -which reports as an error in every preset but `minimal`: a severed docstring +which reports as a warning in every preset but `minimal`: a severed docstring discards its text outright rather than expressing a style preference. ### Severities @@ -320,6 +320,10 @@ A preset name **floats**: it tracks the tool rather than pinning a frozen rule set, so upgrading the tooling can change what a preset reports. To keep that from breaking projects on upgrade, a rule that did not exist before enters existing presets as `"off"`; promoting it is a deliberate, changelogged change. +The one exception so far is `detached_docstring`, which found its way into +`default` directly because its finding is outright discarded program text — and +even that class of rule enters at `"warning"` at most, never `"error"`, so an +upgrade can never change `julialint`'s exit status. Version-pinning syntax (`preset = "default@2"`) may be added later — bare names will keep floating, so nothing written today changes meaning. diff --git a/src/lint_rules.jl b/src/lint_rules.jl index 15b88722..c6d378e4 100644 --- a/src/lint_rules.jl +++ b/src/lint_rules.jl @@ -194,7 +194,10 @@ const LINT_RULES = LintRule[ # ── Purely syntactic rules (see lint_syntax_rules.jl) ──────────────────── # New rules ship `:off` outside `strict` so an upgrade never switches them # on for existing projects; promotion to default-on is a deliberate, - # sweep-validated release decision. + # sweep-validated release decision. The one exception is + # `detached_docstring`, and even it caps at `:warning`: no new rule may + # enter `default` at `:error`, so an upgrade never flips `julialint`'s + # exit code. LintRule(id = :nan_comparison, tier = TierSyntax, severity_default = :off, severity_strict = :warning, doc_link = URI("https://docs.julialang.org/en/v1/base/numbers/#Base.isnan")), @@ -209,9 +212,11 @@ const LINT_RULES = LintRule[ LintRule(id = :async_task, tier = TierSyntax, severity_default = :off, severity_strict = :warning), # The text is discarded outright rather than a style opinion, so it does not - # follow the `:off`-by-default convention for a new rule. + # follow the `:off`-by-default convention for a new rule. `:warning`, not + # `:error`: the detector is a heuristic, and only the definitional + # breakage rules below may fail CI out of the box. LintRule(id = :detached_docstring, tier = TierSyntax, - severity_default = :error, severity_strict = :error), + severity_default = :warning, severity_strict = :warning), # ── Rules backed by analyses other than StaticLint ─────────────────────── LintRule(id = :syntax_errors, tier = TierSyntax, diff --git a/test/test_config.jl b/test/test_config.jl index 9b8e71f3..777c9354 100644 --- a/test/test_config.jl +++ b/test/test_config.jl @@ -206,7 +206,7 @@ end :bare_using => :off, :debug_statement => :off, :async_task => :off, - :detached_docstring => :error, + :detached_docstring => :warning, ) @test JuliaWorkspaces.LINT_PRESETS["default"] == expected_default diff --git a/test/test_lint_syntax_rules.jl b/test/test_lint_syntax_rules.jl index 99838c32..e3eeedf9 100644 --- a/test/test_lint_syntax_rules.jl +++ b/test/test_lint_syntax_rules.jl @@ -290,10 +290,10 @@ end # One finding per detached docstring. @test length(dd_diags("$D\n# c\n$X\n$D\n# c\ng(x) = 2\n")) == 2 - # Reported as an error in the default preset. + # Reported as a warning in the default preset. src = "$D\n# c\n$X\n" d = only(dd_diags(src)) - @test d.severity === :error + @test d.severity === :warning @test occursin("immediately followed", d.message) # The range is exactly the string, not the enclosing container. @test src[first(d.range):last(d.range)-1] == D