From 23668bd6abc39724c0f071c863d164953e63626f Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Thu, 26 Feb 2026 15:05:07 +0000 Subject: [PATCH 1/2] Use explicit StringComparison.Ordinal for string operations throughout codebase Fixes all cases of String.StartsWith, String.EndsWith, String.Contains and String.IndexOf (with string argument) that were missing an explicit StringComparison argument, as identified in issue #742. Changes: - Use StringComparison.Ordinal for all StartsWith/EndsWith comparisons on ASCII/code identifiers and syntax strings - Replace String.Contains with IndexOf(char) for single-char literals (netstandard2.0 compatible) or IndexOf(string, StringComparison.Ordinal) for variable string arguments - Replace ToLower() with ToLowerInvariant() in HtmlParser Files changed: - src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs - src/FSharp.Data.Runtime.Utilities/StructuralInference.fs - src/FSharp.Data.Runtime.Utilities/NameUtils.fs - src/FSharp.Data.Xml.Core/XmlRuntime.fs - src/FSharp.Data.Xml.Core/XmlInference.fs - src/FSharp.Data.Http/Http.fs - src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs - src/FSharp.Data.Csv.Core/CsvRuntime.fs - src/FSharp.Data.Json.Core/JsonSchema.fs - src/FSharp.Data.Html.Core/HtmlOperations.fs - src/FSharp.Data.Html.Core/HtmlParser.fs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Data.Csv.Core/CsvRuntime.fs | 8 ++++++-- src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs | 16 +++++++++++----- src/FSharp.Data.Html.Core/HtmlOperations.fs | 8 ++++++-- src/FSharp.Data.Html.Core/HtmlParser.fs | 4 ++-- src/FSharp.Data.Http/Http.fs | 12 +++++++----- src/FSharp.Data.Json.Core/JsonSchema.fs | 2 +- src/FSharp.Data.Runtime.Utilities/NameUtils.fs | 2 +- .../StructuralInference.fs | 2 +- .../StructuralTypes.fs | 2 +- src/FSharp.Data.Xml.Core/XmlInference.fs | 4 +++- src/FSharp.Data.Xml.Core/XmlRuntime.fs | 7 +++++-- 11 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/FSharp.Data.Csv.Core/CsvRuntime.fs b/src/FSharp.Data.Csv.Core/CsvRuntime.fs index 0dedaf58a..e7a7cbab7 100644 --- a/src/FSharp.Data.Csv.Core/CsvRuntime.fs +++ b/src/FSharp.Data.Csv.Core/CsvRuntime.fs @@ -382,7 +382,7 @@ type CsvFile<'RowType> let probablyTabSeparated = parsedCsvLines.ColumnCount < 2 && noSeparatorsSpecified - && fst parsedCsvLines.FirstLine |> Array.exists (fun c -> c.Contains "\t") + && fst parsedCsvLines.FirstLine |> Array.exists (fun c -> c.IndexOf('\t') >= 0) let parsedCsvLines = if probablyTabSeparated then @@ -474,7 +474,11 @@ type CsvFile<'RowType> |> writeLine (fun item -> let item = item |> nullSafeguard - if item.Contains separator || item.Contains quote || item.Contains "\n" then + if + item.IndexOf(separator, StringComparison.Ordinal) >= 0 + || item.IndexOf(quote, StringComparison.Ordinal) >= 0 + || item.IndexOf('\n') >= 0 + then writer.Write quote writer.Write(item.Replace(quote, doubleQuote)) writer.Write quote diff --git a/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs b/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs index 635552bdb..60e4ec74b 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs @@ -441,7 +441,8 @@ module internal XmlTypeBuilder = [ for child in children -> let isCollectionName parentName childName = - parentName = NameUtils.pluralize childName || parentName.StartsWith childName + parentName = NameUtils.pluralize childName + || parentName.StartsWith(childName, StringComparison.Ordinal) let child = match child with @@ -495,9 +496,9 @@ module internal XmlTypeBuilder = let convFunc = ReflectionHelpers.makeDelegate result.Converter typeof let isCollectionName = - names.[0].EndsWith "List" - || names.[0].EndsWith "Array" - || names.[0].EndsWith "Collection" + names.[0].EndsWith("List", StringComparison.Ordinal) + || names.[0].EndsWith("Array", StringComparison.Ordinal) + || names.[0].EndsWith("Collection", StringComparison.Ordinal) let name = makeUnique ( @@ -527,7 +528,12 @@ module internal XmlTypeBuilder = let convFunc = ReflectionHelpers.makeDelegate result.Converter typeof let name = makeUnique names.[names.Length - 1] - if result.ConvertedType.Name.StartsWith "FSharpOption`1" then + if + result.ConvertedType.Name.StartsWith( + "FSharpOption`1", + StringComparison.Ordinal + ) + then nameWithNS, ProvidedProperty( name, diff --git a/src/FSharp.Data.Html.Core/HtmlOperations.fs b/src/FSharp.Data.Html.Core/HtmlOperations.fs index 9b36270a9..34e12c92b 100644 --- a/src/FSharp.Data.Html.Core/HtmlOperations.fs +++ b/src/FSharp.Data.Html.Core/HtmlOperations.fs @@ -352,11 +352,15 @@ module HtmlNode = selectElements' FilterLevel.Root selectedNodes t | OpenAttribute _ :: AttributeName(_, name) :: EndWith _ :: AttributeValue(_, value) :: CloseAttribute _ :: t -> - let selectedNodes = filterByAttr level acc name (fun v -> v.EndsWith value) + let selectedNodes = + filterByAttr level acc name (fun v -> v.EndsWith(value, StringComparison.Ordinal)) + selectElements' FilterLevel.Root selectedNodes t | OpenAttribute _ :: AttributeName(_, name) :: StartWith _ :: AttributeValue(_, value) :: CloseAttribute _ :: t -> - let selectedNodes = filterByAttr level acc name (fun v -> v.StartsWith value) + let selectedNodes = + filterByAttr level acc name (fun v -> v.StartsWith(value, StringComparison.Ordinal)) + selectElements' FilterLevel.Root selectedNodes t | OpenAttribute _ :: AttributeName(_, name) :: AttributeContainsPrefix _ :: AttributeValue(_, value) :: CloseAttribute _ :: t -> diff --git a/src/FSharp.Data.Html.Core/HtmlParser.fs b/src/FSharp.Data.Html.Core/HtmlParser.fs index 1481fcb45..f1b951198 100644 --- a/src/FSharp.Data.Html.Core/HtmlParser.fs +++ b/src/FSharp.Data.Html.Core/HtmlParser.fs @@ -177,12 +177,12 @@ module internal HtmlParser = x.Tokens <- result :: x.Tokens member x.IsFormattedTag = - match x.CurrentTagName().ToLower() with + match x.CurrentTagName().ToLowerInvariant() with | "pre" -> true | _ -> false member x.IsScriptTag = - match x.CurrentTagName().ToLower() with + match x.CurrentTagName().ToLowerInvariant() with | "script" | "style" -> true | _ -> false diff --git a/src/FSharp.Data.Http/Http.fs b/src/FSharp.Data.Http/Http.fs index 937145bfc..4a1c55cb1 100644 --- a/src/FSharp.Data.Http/Http.fs +++ b/src/FSharp.Data.Http/Http.fs @@ -1740,7 +1740,7 @@ module internal HttpHelpers = | "origin" -> req.Headers.["Origin"] <- value | "pragma" -> req.Headers.[HeaderEnum.Pragma] <- value | "range" -> - if not (value.StartsWith("bytes=")) then + if not (value.StartsWith("bytes=", StringComparison.Ordinal)) then failwithf "Invalid value for the Range header (%O)" value let bytes = value.Substring("bytes=".Length).Split('-') @@ -1821,15 +1821,17 @@ module internal HttpHelpers = let isText (mimeType: string) = let mimeType = mimeType.Trim() - mimeType.StartsWith "text/" + mimeType.StartsWith("text/", StringComparison.Ordinal) || mimeType = HttpContentTypes.Json || mimeType = HttpContentTypes.Xml || mimeType = HttpContentTypes.JavaScript || mimeType = HttpContentTypes.JsonRpc || mimeType = "application/ecmascript" || mimeType = "application/xml-dtd" - || mimeType.StartsWith "application/" && mimeType.EndsWith "+xml" - || mimeType.StartsWith "application/" && mimeType.EndsWith "+json" + || mimeType.StartsWith("application/", StringComparison.Ordinal) + && mimeType.EndsWith("+xml", StringComparison.Ordinal) + || mimeType.StartsWith("application/", StringComparison.Ordinal) + && mimeType.EndsWith("+json", StringComparison.Ordinal) mimeType.Split([| ';' |], StringSplitOptions.RemoveEmptyEntries) |> Array.exists isText @@ -2009,7 +2011,7 @@ type Http private () = | [] -> url | query -> url - + if url.Contains "?" then "&" else "?" + + if url.IndexOf('?') >= 0 then "&" else "?" + String.concat "&" [ for k, v in query -> Uri.EscapeDataString k + "=" + Uri.EscapeDataString v ] static member private InnerRequest diff --git a/src/FSharp.Data.Json.Core/JsonSchema.fs b/src/FSharp.Data.Json.Core/JsonSchema.fs index a83a75b4c..784aa0e6b 100644 --- a/src/FSharp.Data.Json.Core/JsonSchema.fs +++ b/src/FSharp.Data.Json.Core/JsonSchema.fs @@ -319,7 +319,7 @@ module JsonSchema = // This is a simplified implementation - a complete one would handle JSON pointers properly let rec resolveRef (refPath: string) = match refPath with - | path when path.StartsWith("#/") -> + | path when path.StartsWith("#/", StringComparison.Ordinal) -> // Handle local references like "#/definitions/Point" let parts = path.Substring(2).Split('/') diff --git a/src/FSharp.Data.Runtime.Utilities/NameUtils.fs b/src/FSharp.Data.Runtime.Utilities/NameUtils.fs index 502621922..34ad55323 100644 --- a/src/FSharp.Data.Runtime.Utilities/NameUtils.fs +++ b/src/FSharp.Data.Runtime.Utilities/NameUtils.fs @@ -117,7 +117,7 @@ let uniqueGenerator (niceName: string -> string) = lastLetterPos <- lastLetterPos - 1 if lastLetterPos = name.Length - 1 then - if name.Contains " " then + if name.IndexOf(' ') >= 0 then name <- name + " 2" else name <- name + "2" diff --git a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs index ad7a68efa..39236579c 100644 --- a/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs +++ b/src/FSharp.Data.Runtime.Utilities/StructuralInference.fs @@ -413,7 +413,7 @@ let parseUnitOfMeasure (provider: IUnitsOfMeasureProvider) (str: string) = uomTransformations |> List.collect (fun (suffixes, trans) -> suffixes |> List.map (fun suffix -> suffix, trans)) |> List.tryPick (fun (suffix, trans) -> - if str.EndsWith suffix then + if str.EndsWith(suffix, StringComparison.Ordinal) then let baseUnitStr = str.[.. str.Length - suffix.Length - 1] let baseUnit = provider.SI baseUnitStr diff --git a/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs b/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs index e8b1df393..c80050324 100644 --- a/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs +++ b/src/FSharp.Data.Runtime.Utilities/StructuralTypes.fs @@ -178,7 +178,7 @@ type internal InferedTypeTag with /// Parses code returned by 'Code' member (to be used in provided code) static member ParseCode(str: string) = match str with - | s when s.StartsWith("Record@") -> Record(Some(s.Substring("Record@".Length))) + | s when s.StartsWith("Record@", StringComparison.Ordinal) -> Record(Some(s.Substring("Record@".Length))) | "Record" -> Record None | "Json" -> Json | "Number" -> Number diff --git a/src/FSharp.Data.Xml.Core/XmlInference.fs b/src/FSharp.Data.Xml.Core/XmlInference.fs index b4374a563..96828e6f7 100644 --- a/src/FSharp.Data.Xml.Core/XmlInference.fs +++ b/src/FSharp.Data.Xml.Core/XmlInference.fs @@ -45,7 +45,9 @@ let getInferedTypeFromValue unitsOfMeasureProvider inferenceMode cultureInfo (el | InferedType.Primitive(t, _, optional, _) when t = typeof && let v = (element.Value).TrimStart() in - v.StartsWith "{" || v.StartsWith "[" + + v.StartsWith("{", StringComparison.Ordinal) + || v.StartsWith("[", StringComparison.Ordinal) -> try match JsonValue.Parse(element.Value) with diff --git a/src/FSharp.Data.Xml.Core/XmlRuntime.fs b/src/FSharp.Data.Xml.Core/XmlRuntime.fs index 20c28ff55..e89c8d35a 100644 --- a/src/FSharp.Data.Xml.Core/XmlRuntime.fs +++ b/src/FSharp.Data.Xml.Core/XmlRuntime.fs @@ -116,7 +116,7 @@ type XmlElement = (parseWithReader text).Root.Elements() |> Seq.map (fun value -> { XElement = value }) |> Seq.toArray - with _ when text.TrimStart().StartsWith "<" -> + with _ when text.TrimStart().StartsWith("<", System.StringComparison.Ordinal) -> (parseWithReader ("" + text + "")).Root.Elements() |> Seq.map (fun value -> { XElement = value }) |> Seq.toArray @@ -380,7 +380,10 @@ module XmlSchema = let uri = // Uri must end with separator (maybe there's a better way) if resolutionFolder = "" then "" - elif resolutionFolder.EndsWith "/" || resolutionFolder.EndsWith "\\" then + elif + resolutionFolder.EndsWith("/", StringComparison.Ordinal) + || resolutionFolder.EndsWith("\\", StringComparison.Ordinal) + then resolutionFolder else resolutionFolder + "/" From e6c91be86879fae63b554da145684b913a4e99cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Feb 2026 15:07:42 +0000 Subject: [PATCH 2/2] ci: trigger CI checks