diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index b066cf7310..57c4ce038e 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Python] Fix missing `Array` module implementations and tests (by @ncave) * [Python] Fix object expressions implementing interfaces with `[]` members no longer produce unimplementable abstract Protocol members (fixes #3039) * [Python] Fix `DateTime.TryParse` incorrectly assigning `DateTimeKind.Local` to naive datetime strings (should be `DateTimeKind.Unspecified`) (fixes #3654) +* [JS/TS] Fix `String.Contains` ignoring `StringComparison` argument (second argument was silently discarded) +* [Python] Fix `String.Contains` ignoring `StringComparison` argument (second argument was silently discarded) ## 5.0.0-rc.7 - 2026-04-07 diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 8bb490a126..aa51fd68e8 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Python] Fix missing `Array` module implementations and tests (by @ncave) * [Python] Fix object expressions implementing interfaces with `[]` members no longer produce unimplementable abstract Protocol members (fixes #3039) * [Python] Fix `DateTime.TryParse` incorrectly assigning `DateTimeKind.Local` to naive datetime strings (should be `DateTimeKind.Unspecified`) (fixes #3654) +* [JS/TS] Fix `String.Contains` ignoring `StringComparison` argument (second argument was silently discarded) +* [Python] Fix `String.Contains` ignoring `StringComparison` argument (second argument was silently discarded) ## 5.0.0-rc.13 - 2026-04-07 diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 2f5e662802..24b07ce97d 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1474,13 +1474,13 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt makeEqOp r left (makeIntConst 0) BinaryEqual |> Some | "GetEnumerator", Some c, _ -> getEnumerator com r t c |> Some - | "Contains", Some c, arg :: _ -> - if (List.length args) > 1 then - addWarning com ctx.InlinePath r "String.Contains: second argument is ignored" - + | "Contains", Some c, [ arg ] -> let left = Helper.InstanceCall(c, "find", Int32.Number, [ arg ]) makeEqOp r left (makeIntConst 0) BinaryGreaterOrEqual |> Some + | "Contains", Some c, [ _arg; _comp ] -> + Helper.LibCall(com, "string", "contains", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some | "StartsWith", Some c, [ _str ] -> Helper.LibCall(com, "string", "starts_with_exact", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index cd820e52e8..c9faa4693a 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1647,12 +1647,12 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt makeEqOp r left (makeIntConst 0) BinaryEqual |> Some | "GetEnumerator", Some c, _ -> stringToCharArray c |> getEnumerator com r t |> Some - | "Contains", Some c, arg :: _ -> - if (List.length args) > 1 then - addWarning com ctx.InlinePath r "String.Contains: second argument is ignored" - + | "Contains", Some c, [ arg ] -> let left = Helper.InstanceCall(c, "indexOf", Int32.Number, [ arg ]) makeEqOp r left (makeIntConst 0) BinaryGreaterOrEqual |> Some + | "Contains", Some c, [ _arg; _comp ] -> + Helper.LibCall(com, "String", "contains", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> Some | "StartsWith", Some c, [ _str ] -> Helper.InstanceCall(c, "startsWith", Boolean, args) |> Some | "StartsWith", Some c, [ _str; _comp ] -> Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) diff --git a/src/fable-library-py/fable_library/string_.py b/src/fable-library-py/fable_library/string_.py index eff1689c45..8c45ead7ae 100644 --- a/src/fable-library-py/fable_library/string_.py +++ b/src/fable-library-py/fable_library/string_.py @@ -114,6 +114,18 @@ def starts_with_exact(string: str, pattern: str) -> bool: return idx == 0 +def contains(string: str, value: str, comparison: int) -> bool: + """Check if string contains value using the given StringComparison.""" + ignore_case = comparison in ( + StringComparison.CURRENT_CULTURE_IGNORE_CASE, + StringComparison.INVARIANT_CULTURE_IGNORE_CASE, + StringComparison.ORDINAL_IGNORE_CASE, + ) + if ignore_case: + return value.casefold() in string.casefold() + return value in string + + def index_of_any(string: str, any_of: Array[str], *args: int) -> int: """Find index of any character from the array.""" if not string: @@ -151,6 +163,7 @@ def get_length(string: str) -> int32: "compare", "compare_to", "concat", + "contains", "continue_print", "ends_with", "ends_with_exact", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index a1429d0be3..117790cf90 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +* [JS/TS] Fix `String.Contains` ignoring `StringComparison` argument (second argument was silently discarded) + ## 2.0.0-rc.6 - 2026-04-07 ### Fixed diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 2dec882967..1049fb6d50 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -86,6 +86,19 @@ export function endsWith(str: string, pattern: string, ic: boolean | StringCompa return false; } +export function contains(str: string, pattern: string, ic: boolean | StringComparison) { + if (ic === StringComparison.Ordinal) { // fast path + return str.includes(pattern); + } + const len = pattern.length; + for (let i = 0; i <= str.length - len; i++) { + if (cmp(str.slice(i, i + len), pattern, ic) === 0) { + return true; + } + } + return false; +} + export function indexOfAny(str: string, anyOf: string[], ...args: number[]) { if (str == null || str === "") { return -1; diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 5558a37559..8f430baf86 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -702,6 +702,11 @@ let tests = testList "Strings" [ "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false + testCase "String.Contains with StringComparison works" <| fun () -> + "ABC".Contains("b", StringComparison.Ordinal) |> equal false + "ABC".Contains("b", StringComparison.OrdinalIgnoreCase) |> equal true + "ABC".Contains("b", StringComparison.InvariantCultureIgnoreCase) |> equal true + testCase "String.Split works" <| fun () -> "a b c d".Split(' ') |> equal [|"a";"b";"c";"";"d"|] diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index 925ddd707e..65a4cb0139 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -487,6 +487,12 @@ let ``test String.Contains works`` () = "ABC".Contains("B") |> equal true "ABC".Contains("Z") |> equal false +[] +let ``test String.Contains with StringComparison works`` () = + "ABC".Contains("b", StringComparison.Ordinal) |> equal false + "ABC".Contains("b", StringComparison.OrdinalIgnoreCase) |> equal true + "ABC".Contains("b", StringComparison.InvariantCultureIgnoreCase) |> equal true + [] let ``test String.Split works`` () = "a b c d".Split(' ')