Module: StillActive::DotnetHelper
Overview
The NuGet calibration layer for the language-runtime ceiling (SA009), the .NET sibling of RubyHelper/PythonHelper. NuGet is shaped differently from Ruby/Python: a package declares not one version RANGE but a SET of target framework monikers (net6.0, netcoreapp3.1, net48, netstandard2.0). Targeting a framework is an enforced restore-time wall (NuGet error NU1202), so if EVERY concrete runtime the package targets is end-of-life, consuming it forces your project onto a dead runtime -- the same "no supported runtime is reachable" cap the Ruby/Python ceiling catches, expressed as a set rather than a range.
The honesty rule is the netstandard exclusion: netstandard* is a
compatibility contract consumable FROM a modern .NET, not a runtime, so it
imposes no ceiling. Without excluding it the signal would fire on nearly every
modern library. We therefore fire only when the package has at least one
concrete runtime target AND every concrete runtime target is EOL.
Instance Method Summary collapse
-
#analyze(target_frameworks:, dotnet:, dotnetfx:) ⇒ Object
=> the SA009 eol_forced finding hash, or nil when the package is not capped.
-
#classify(moniker) ⇒ Object
A target framework moniker -> { family: :dotnet | :dotnetfx, version: } (the runtime it names and the endoflife.date cycle to look it up by), or nil when the moniker is not a concrete runtime (netstandard, or unrecognized -- we never guess).
-
#netstandard?(moniker) ⇒ Boolean
Whether a moniker is a netstandard target (tolerating the long-form ".NETStandard" and platform/case variants) -- the escape hatch analyze must honour.
-
#supported_dotnet_range ⇒ Object
The two .NET runtime calendars, fetched once per SBOM.
- #supported_dotnetfx_range ⇒ Object
Instance Method Details
#analyze(target_frameworks:, dotnet:, dotnetfx:) ⇒ Object
=> the SA009 eol_forced finding hash, or nil when the package is not capped.
dotnet / dotnetfx are EndoflifeHelper.support_window results (or nil when
the feed is down); only their :cycles list is read.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/helpers/dotnet_helper.rb', line 76 def analyze(target_frameworks:, dotnet:, dotnetfx:) monikers = Array(target_frameworks) # THE honesty rule: a netstandard target is a compatibility contract # consumable from a current .NET, so its mere presence is an escape hatch -- # the package is not capped, whatever else it targets. Without this the signal # fires on nearly every modern .NET library (Newtonsoft.Json, EF Core, ...), # which all ship a netstandard2.0 target alongside older concrete runtimes. return if monikers.any? { |moniker| netstandard?(moniker) } runtimes = monikers.filter_map do |moniker| classification = classify(moniker) classification&.merge(moniker: moniker, cycle: cycle_for(classification, dotnet: dotnet, dotnetfx: dotnetfx)) end return if runtimes.empty? # A still-supported runtime target is a clear escape hatch: not capped. return if runtimes.any? { |r| r[:cycle] && r[:cycle][:eol] == false } eol = runtimes.select { |r| r[:cycle] && r[:cycle][:eol] == true } return if eol.empty? # nothing KNOWN-EOL: don't fabricate a ceiling from unknown cycles # The longest-lived dead runtime is the best the package offers -- the ceiling. ceiling = eol.max_by { |r| r[:cycle][:eol_date] || Time.at(0) } finding = { requirement: runtimes.map { |r| r[:moniker] }.uniq.sort.join(", "), eol_forced: true, runtime: ceiling[:family] == :dotnetfx ? ".NET Framework" : ".NET", ceiling_version: ceiling[:version].to_s, ceiling_eol_date: ceiling[:cycle][:eol_date], } finding.merge(severity: ConstraintHelper.constraint_severity(finding)) end |
#classify(moniker) ⇒ Object
A target framework moniker -> { family: :dotnet | :dotnetfx, version: } (the runtime it names and the endoflife.date cycle to look it up by), or nil when the moniker is not a concrete runtime (netstandard, or unrecognized -- we never guess). Tolerates deps.dev's short form (net45, netcoreapp3.1), its long form (.NETFramework4.5), platform suffixes (net6.0-windows), and case.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/helpers/dotnet_helper.rb', line 35 def classify(moniker) return if moniker.nil? m = moniker.to_s.strip.downcase return if m.empty? # Drop a platform suffix (net6.0-windows -> net6.0) and the long-form dot. dash = m.index("-") m = m[0, dash] if dash m = m.delete_prefix(".") return if m.start_with?("netstandard") # a compatibility contract, not a runtime # Ordered, mutually-exclusive prefixes; the load-bearing distinction is the # dot: a dotted "netX.Y" is .NET 5+ (major cycle), a dot-less "netXY" is .NET # Framework (digits expand to the cycle). if m.start_with?("netcoreapp") dotnet(m.delete_prefix("netcoreapp")) # netcoreapp3.1 -> .NET "3.1" (minor kept) elsif m.start_with?("netframework") dotnetfx(m.delete_prefix("netframework")) # long form: .NETFramework4.7.2 elsif m.start_with?("net") rest = m.delete_prefix("net") if rest.include?(".") dotnet(rest.split(".").first) # net6.0 -> .NET "6" (major only) elsif all_digits?(rest) dotnetfx((rest)) # net48 -> .NET Framework "4.8" end end end |
#netstandard?(moniker) ⇒ Boolean
Whether a moniker is a netstandard target (tolerating the long-form ".NETStandard" and platform/case variants) -- the escape hatch analyze must honour.
67 68 69 70 71 |
# File 'lib/helpers/dotnet_helper.rb', line 67 def netstandard?(moniker) return false if moniker.nil? moniker.to_s.strip.downcase.delete_prefix(".").start_with?("netstandard") end |
#supported_dotnet_range ⇒ Object
The two .NET runtime calendars, fetched once per SBOM. .NET Core/5+ is a
single unified endoflife feed; .NET Framework 4.x is its own (dotnetfx),
with a different EOL schedule tied to Windows servicing.
27 |
# File 'lib/helpers/dotnet_helper.rb', line 27 def supported_dotnet_range = EndoflifeHelper.support_window(feed_path: "/api/dotnet.json") |
#supported_dotnetfx_range ⇒ Object
28 |
# File 'lib/helpers/dotnet_helper.rb', line 28 def supported_dotnetfx_range = EndoflifeHelper.support_window(feed_path: "/api/dotnetfx.json") |