What's Changed
New Features
- Added manual update checks in Settings: Check now bypasses the automatic
daily gate, shows checking/current/download/error states, verifies and
stages updates, and offers Later or Perform update to apply and
relaunch with What's New on the updated launch.
Internal
-
Qodana's false-positive suppressions are confirmed in CI, not just under
GoLand's engine: at210fee5(run33270269940), the summary CSV counts
the 12 findings covered by the 8//goland:noinspectioncomments exactly —
GoMaybeNil2,GoBoolExpressions3,GoRedundantConversion1,
GoErrorStringFormat2,GoVarAndConstTypeMayBeOmitted4, summing to 12 —
and none of the 12 appear anywhere inqodana.sarif.json's results, whose
onlyruleIdvalues areDuplicatedCodeandGoTypeAssertionOnErrors:
12-of-12 suppressed. The other 18 suppressions, theGoUnusedParameter_
renames, never produced a CSV row at all — the blank identifier removes the
finding before the inspection pass counts it, rather than suppressing a
counted one. The earlier prediction that the next CI run would show "30
fewer problems (155 -> 125 on a full scan)" was wrong, and not because the
suppressions failed: 155 was an IDE Project Default scan and 125 assumed CI
would run that same profile, but CI runsqodana.starterinstead, so an
IDE Project Default total and a CIqodana.startertotal were never
comparable in the first place. -
CI does not under-report duplication against a full scan — the old framing
had it backwards. Qodana emits one SARIF result per duplicate cluster,
not per fragment: at210fee5the SARIF holds 33DuplicatedCoderesults
occupying 71 location slots, and the slots are not disjoint because 8
fragments each belong to 2 clusters (55 fragments in exactly 1 cluster, 8
in exactly 2: 55×1 + 8×2 = 71), so 71 slots reduce to 63 distinct
fragments. The summary CSV counts 75DuplicatedCoderows, and the gap
closes completely rather than leaving a mystery: 75 CSV rows = 71
test-file fragments + 4 production fragments already suppressed at source
in the orientation pixel loops (see Orientation transforms below), and 71
− 8 serialisation losses = 63 SARIF fragments, so the 12-fragment
CSV-to-SARIF gap is 8 + 4 with nothing left unexplained. At210fee5the
IDE finds 71DuplicatedCodefragments and CI finds 63; CI's 63 are a
strict subset of the IDE's 71, and the 8 fragments CI is missing — 7 in
internal/imaging/loader_test.goand 1 ininternal/update/tufroot_test.go
— are accounted for by a genuine Qodana serialisation failure that the CI
run recorded itself as 3DuplicatesProblem"Can't find duplicate problem
in db" warnings naming exactly those 2 files and no others. All 33
clusters at210fee5were test-only — every fragment in every cluster
lives in a_test.gofile — soqodana.yamlnow excludes those files from
DuplicatedCodeby explicit path; run33274422606ated3d4e6confirmed
it, returning 0 SARIF results. -
Making the
DuplicatedCodeexclusion actually take effect needed three
attempts, but the mechanism itself was never broken — only the pattern.
exclude:with apaths:glob compiles into a real scope:
log/effective.profile.xmlshows<scope name="qodana.yaml.exclude.DuplicatedCode" level="INFORMATION" enabled="false" />nested inside theDuplicatedCodeinspection element
itself, proving the entry reached the engine — but a compiled, disabled
scope that matches no file suppresses nothing. Both"**/*_test.go"(run
33273666731atd38f6e8) and"**_test.go"(run33274030031at
f481c4a) failed exactly that way: 33DuplicatedCoderesults both times,
unchanged from baseline. The second glob is the dialect JetBrains uses for
its own built-in scopes (glob:**.md,glob:**.test.ts), so it was not a
wild guess, and it still did not match. Neither failure was a delivery
problem — each run'slog/qodana-config.jsonechoes back the exact
pattern that run was given, and neither run'sidea.logshows a cache hit
or restore. Explicit file paths work: withqodana.yaml'spaths:
replaced by the 30 flagged test files listed by path, run33274422606at
ed3d4e6returned 0 SARIF results. That the CSV'sDuplicatedCodecount
fell to 4 rather than 0 on that run is the proof the exclusion narrowed the
inspection instead of disabling it outright — a full disable would have
zeroed the CSV too, not just the SARIF. -
Update-check lifecycle ordering is restored: after its existing gates,
maybeStartUpdateCheckprepares the verifier/client through the
instance-ownedUpdaterverifier-factory seam beforeupdateOp.begin(),
then callsUpdater.Start. The focused
TestUpdateCheck_VerifierFailurePreservesLifecycleregression test proves
a verifier failure preserves the revision and prior token/context, leaves
the client nil, and never begins completion. Movingbeginabove
EnsureClientmakes that test fail on the lifecycle preservation; restoring
the intended order passes. -
buildMainMenu's exacthelp.SetOnManualOpened(view.syncMenus)observer
registration is covered by
TestBuildMainMenu_ManualOpenedObserverSyncsWindowHelp. The test scopes
theme.DefaultTheme()to this case because the shared Fyne test theme
cannot render the manual's complete Markdown font combinations; it identifies
and closes the newly created manual window and restores the original theme
during cleanup. Removing the observer registration makes the test fail,
leaving Window → Help enabled after the manual opens; the restored
registration passes. -
Favorites no longer un-merges the macOS native menu bar. The original note
here had the diagnosis half wrong, so for the record:SetHasFileswas
never an unguarded site -syncMenusfolds on the very next line,
deliberately -SetDiris covered by the startup fold atrun.go:45, and
there is no rename path at all, sincefavstoreexposes onlySaveand
Remove. The two real paths were adding a favorite (favorites.go:249)
and deleting one (manage.go:362), both reachingrefreshMenuwith
nothing to fold the bar afterwards.favorites.Hostgained
RefreshMenus, which the viewer implements asrefreshMainMenu;
refreshMenucalls it instead off.menu.Refresh, andSetHasFilesnow
only flipsDisabled. The invariant is structural rather than documented:
internal/ui/favoritesnever callsfyne.Menu.Refresh, so
refreshMainMenuis the only place a main-bar menu is re-published.
Pinned at both ends - three tests infavoritesthat the feature makes
the call (and thatSetHasFilesdoes not), one ininternal/uithat the
viewer's method reachesrefreshMainMenu. Fyne's test driver records
nothing when the bar is re-published:MainMenu.Refreshre-hands the same
pointer totest.window.SetMainMenu, which just stores it, and both
syncNativeMenuBarsteps dead-end on NSApp's nil menus in a test binary.
The only trace isrefreshMainMenureading the window's menu, so the
viewer test observes that through a decorator on the per-viewerv.win.
All four verified by mutation. -
Small mechanical Qodana fixes, five categories.
imaging/save.goand
exifwin/tiles_test.gocompare errors witherrors.Isnow;
update/extract.goandplistdoctypes/doctypes_test.godeliberately keep
err == io.EOF, because the inspection does not flag the documented
tar.Reader.Next/xml.Decoder.Tokenloop terminators and the bare
sentinel is what those APIs promise.raw_test.godrops a redundant
[]byteconversion (copytakes a string source).apply_test.go's
localrealbecametarget, filename string included, so it no longer
shadows the builtin. The two scripts mark their ignoredFprintfresults
_, _ =, matchingscripts/plistdoctypes/main.go. The spiral FPS
backdrop colours were the only non-trivial one: they were positional
color.NRGBAliterals written out in bothoverlays.goand
overlays_test.go, so they moved into package-level
fpsGoodColor/fpsWarnColor/fpsBadColorthat both sides name. That
alone would have gutted the test - it would compare a var against itself
and only catch a wrong threshold - soTestFPSBackdropColorValueswas
added to pin the values, and verified by mutation: bumping
fpsGoodColor's G to 121 fails it while the threshold test still passes. -
Doc-comment openings:
GoCommentStartwants the documented element's name
as the comment's first token, followed by whitespace - an optional leading
A/An/Theis the only thing allowed in front. Punctuation glued to
the name breaks it - a slash, a comma, or a colon all fail even when the
comment already opens with the right word, which is whyDir/SetDirand
MouseIn, MouseMoved, and MouseOutwere both flagged. The slash-joined
pairs became theX and Yformzoom.In/Out,
widgets.FocusGained/FocusLostandimaging.MinVectorWidth/Height
already used; the threewidgets/style.gogroup comments now lead with
the first constant in theirconst/vargroup; andinternal/ui's five
file-level notes gained a blank line beforepackage ui, which detaches
them from the package clause - the form 69 other files in the tree already
use, withrun.gokeeping the one real package doc.
spiral.ShowForGestureandShowshared one comment block that opened by
describingShow, so it was split:go doc Spiral.Showprinted nothing
before and prints its paragraph now. Also normalized the six unflagged
sites with the same shape inload.goandpreferences.go; the
inspection skips struct fields and mid-comment references, so those were
consistency rather than findings. Verified through GoLand's inspection
engine over all 13 files, which is the same engine Qodana runs and needs
noQODANA_TOKEN. -
Test image fixture duplication:
wrapAPP1now owns Exif APP1 framing,
littleEndianTIFFshares TIFF header and integer emission across the
capture-date, RAW-preview, and GPS fixtures, and imaging's oversized-PNG
test reusesuitest.TruncatedPNGHeader; byte-level tests lock both helper
formats. -
Orientation transforms: the five direct pixel loops stay separate to avoid
callback dispatch or transform branching in the per-pixel hot path. The
four reportedDuplicatedCodecopies carry source-local suppressions and
explanations; characterization tests cover offset source bounds. -
JPEG header-segment walk:
walkJPEGSegmentsininternal/imaging/jpegseg.go
is the one SOI-to-SOS marker loop;jpegEXIFOrientation,jpegMetadata,
jpegMetadataSegments, andjpegHasRemovableMetadatakeep only their
per-segment bodies.stripJPEGSegmentsandjpegLengthstay separate —
they copy/error and walk entropy-coded scans, respectively. -
Settings numeric entries:
newPositiveIntEntryin
internal/ui/settingswin/settingswin.gois the one positive-int Entry
constructor; max-scan, max-width, max-height, image-cache, thumb-cache,
and max-file-size keep only their Host getter/setter (and the float32
wrap on window size). The picture-frame interval stays a ParseInt +
Duration path.
Full Changelog: v0.2.11...v0.2.12