Compare semantic strings by value and expose path values on interfaces - #140
Merged
Conversation
…nterfaces SemanticString<T>.CompareTo(object) forwarded straight to string.CompareTo(object), which only accepts a System.String. Sorting a collection typed as an interface that carries no IComparable<T> — such as IPath — makes LINQ and Comparer<T>.Default fall back to that non-generic overload and hand it another semantic string, so the sort threw ArgumentException: "Object must be of type String." Reported downstream as ktsu-dev/ImGuiApp#273, where it crashed every file dialog on a directory listing. CompareTo(object) now unwraps ISemanticString and compares underlying values. Null still sorts first and unsupported types still throw ArgumentException. Also addresses why that sort reached the boxing path at all: - IPath, IFileName, IFileExtension and IDirectoryName were empty marker interfaces, so code holding the IEnumerable<IPath> returned by GetContents() could not read a path's value without downcasting to a concrete type. All four now expose WeakString, which every implementation already had via SemanticString<TDerived>. - IPath declares IComparable<IPath>, so Comparer<IPath>.Default now sorts through the generic comparison path. SemanticPath<TDerived> implements it explicitly: a public overload taking IPath would make pathA.CompareTo(pathB) ambiguous against the inherited overload taking ISemanticString, since a concrete path satisfies both parameter types and neither is more specific. Adds *.cs text eol=crlf to .gitattributes to match .editorconfig, mirroring ktsu-dev/ImGuiApp@dd44580. Without it a Linux checkout gets LF working files that violate end_of_line = crlf, and the repo cannot build at all — 1711 IDE0055 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFRwYRHhEmVMGvrH2BbtbW
The generators wrote through ktsu.CodeBlocker, which uses IndentedTextWriter and
therefore Environment.NewLine, so identical metadata produced CRLF output on Windows
and LF output on Linux. Generator output under Semantics.Quantities/Generated/ is
committed and CI-verified, so it has to be host-independent. All 12 AddSource call
sites now go through GeneratedSource.Add, which normalizes to CRLF — matching
end_of_line = crlf and the *.cs eol=crlf attribute. Regenerating from scratch
produces all 226 files as CRLF with no content change.
Semantics.Paths, Semantics.Strings and Semantics.Quantities each disabled package
validation because ktsu.Sdk 2.11.0 turned on strict ApiCompat, whose
compatible-frameworks check cannot accept the by-design per-TFM surface differences
that Polyfill shims produce. ktsu.Sdk 2.15.0 — what this repo pins — already leaves
EnableStrictModeForCompatibleFrameworksInPackage and EnableStrictModeForCompatibleTfms
off for exactly that reason, and directs the residual diffs to a regenerable
CompatibilitySuppressions.xml. So validation is back on in all three, with
suppressions regenerated via -p:ApiCompatGenerateSuppressionFile=true.
The regenerated suppression files are 16978 lines smaller: the old ones were dominated
by entries for net5.0/net6.0/net7.0 assets this package no longer ships. What remains
is real and per-TFM by design — CP0005 on record {Clone}$ members, CP0014/CP0015 on
Polyfill shim attributes, CP0006 on the netstandard2.1-only FromReadOnlySpan.
Note that PackageValidationBaselineVersion remains unset, so this gate covers
cross-TFM consistency within a package but does not yet compare against the published
version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFRwYRHhEmVMGvrH2BbtbW
The tests I added hard-coded Unix paths, so on Windows CI every one of them threw ArgumentException: Cannot convert "\tmp\afile" to AbsoluteFilePath — /tmp/afile canonicalizes to \tmp\afile, which is not absolute without a drive or UNC prefix. That is the same defect as the suite's pre-existing Linux failures, just mirrored. Paths are now built from Path.GetPathRoot(Path.GetTempPath()), giving C:\ on Windows and / elsewhere, and the expected values are derived from the same helper rather than written as literals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFRwYRHhEmVMGvrH2BbtbW
|
matt-edmondson
added a commit
to ktsu-dev/ImGuiApp
that referenced
this pull request
Jul 30, 2026
2.8.0 fixes SemanticString<T>.CompareTo(object) so it unwraps ISemanticString rather than handing another semantic string to string.CompareTo(object) (ktsu-dev/Semantics#140), and exposes WeakString on IPath, IFileName, IFileExtension and IDirectoryName. Nothing here was required by the upgrade — ImGuiApp implements none of those interfaces, so the added members break nothing, and the build passes untouched against 2.8.0. These are the simplifications the new surface allows: - DrawContentRow read the display path by testing for two concrete types and falling back to string.Empty, a branch that could never legitimately be reached. IAbsolutePath now carries WeakString directly. - SortContents and its tests used ToString(), which returns string? and cost a null-forgiving operator at every call site. SortContents still sorts on the string value rather than the entry. The IComparable<IPath> that 2.8.0 declares only helps collections typed as IPath exactly; CurrentContents is a Collection<IAbsolutePath>, which implements IComparable<T> of neither itself nor IPath, so sorting by the entry would still box through the non-generic CompareTo(object). Sorting on the string value also keeps case-insensitive display order. All four Semantics packages move together since that repo versions in lockstep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFRwYRHhEmVMGvrH2BbtbW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


The bug
SemanticString<T>.CompareTo(object)forwarded straight tostring.CompareTo(object), which accepts only aSystem.String:Sorting a collection typed as an interface that carries no
IComparable<T>— such asIPath— makes LINQ andComparer<T>.Defaultfall back to that non-generic overload and hand it another semantic string, not a string. Result:Reported downstream as ktsu-dev/ImGuiApp#273, where it crashed every file dialog as soon as the listed directory held two entries of the same kind. Not platform-specific despite the report title — it reproduces anywhere.
CompareTo(object)now unwrapsISemanticStringand compares underlying values.nullstill sorts first and unsupported types still throwArgumentException.Why the sort reached the boxing path at all
IPath,IFileName,IFileExtensionandIDirectoryNamewere empty marker interfaces. SinceSemanticDirectoryPath.GetContents()returnsIEnumerable<IPath>, the library handed consumers a type from which the path value could not be recovered without a downcast. That is what the downstream code had to write:WeakString. Every implementation already had it viaSemanticString<TDerived>, so no implementation changed.ToString()was the only prior option and returnsstring?, costing every call site a!.IPathdeclaresIComparable<IPath>, soComparer<IPath>.Defaultresolves toGenericComparerinstead ofObjectComparerand never boxes into the non-generic path.SemanticPath<TDerived>implements it explicitly — a public overload takingIPathwould makepathA.CompareTo(pathB)ambiguous against the inherited overload takingISemanticString, because a concrete path satisfies both parameter types and neither is more specific. There's a test whose failure to compile is the regression signal for that.Build fix
.gitattributesgains*.cs text eol=crlfto match.editorconfig, mirroring ktsu-dev/ImGuiApp@dd44580. Without it a Linux checkout produces LF working files that violateend_of_line = crlfand the repo cannot build at all — 1711IDE0055errors before a single line of real work.Tests
16 new tests:
CompareTo(object)with semantic strings and unsupported types, sorting viaOrderBy/ThenBy/Comparer<T>.Default/List<IPath>.Sort(),WeakStringthrough each interface,GetContents()read without downcasting, and the comparer-selection and no-ambiguity guards. Reverting theCompareTofix fails 5 of them.977 passed, 93 failed— the 93 are pre-existing and unrelated: path tests hard-coding Windows separators andC:\roots, which cannot pass on Linux. Verified by stashing this branch and re-running on a clean tree: identical 93. Baseline was 961 passing.Packs clean (
Semantics.PathssetsEnablePackageValidation=false, so there's no baseline API check to trip).Note on the version tag
Tagged
[minor]. Adding members to a public interface is a source break for anyone implementing these interfaces outside the library — though no public API accepts anIPath(or any descendant) as a parameter, so an external implementation has nothing to plug into today. If you'd rather treat interface-member additions as major, retag before merge.🤖 Generated with Claude Code
https://claude.ai/code/session_01WFRwYRHhEmVMGvrH2BbtbW