Skip to content

Commit 048f27e

Browse files
authored
[JS/TS] Add nullness supports (#4070)
* [JS/TS] add tests for nullness supports [JS/TS] Add support for `Unchecked.nonNull` * [All] Make Fable report nullness warning if enable on the project * Capture `Nullable` flag only for the main project * chore: update changelog * add changelog entry
1 parent e96be5a commit 048f27e

File tree

8 files changed

+118
-0
lines changed

8 files changed

+118
-0
lines changed

.fantomasignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
src/fcs-fable
22
tests/**
33
tests_external/**
4+
5+
!tests/Js/Main/Nullness.fs

src/Fable.Cli/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
* [Python] Add support for `nullArgCheck`(by @MangelMaxime)
13+
* [All] Add support for F# `nullness` (by @MangelMaxime)
14+
* [JS/TS] Add support for `Unchecked.nonNull` (by @MangelMaxime)
1315

1416
### Fixed
1517

src/Fable.Compiler/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
* [Python] Add support for `nullArgCheck`(by @MangelMaxime)
13+
* [All] Add support for F# `nullness` (by @MangelMaxime)
14+
* [JS/TS] Add support for `Unchecked.nonNull` (by @MangelMaxime)
1315

1416
### Fixed
1517

src/Fable.Compiler/ProjectCracker.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ let private extractUsefulOptionsAndSources
427427
accSources, line :: accOptions
428428
else
429429
accSources, accOptions
430+
// Only forward the nullness flag if it is coming from the main project
431+
elif line.StartsWith("--checknulls+", StringComparison.Ordinal) && isMainProj then
432+
accSources, line :: accOptions
430433
elif
431434
line.StartsWith("--nowarn", StringComparison.Ordinal)
432435
|| line.StartsWith("--warnon", StringComparison.Ordinal)

src/Fable.Transforms/Replacements.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2979,6 +2979,7 @@ let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option)
29792979
| "Hash", [ arg ] -> structuralHash com r arg |> Some
29802980
| "Equals", [ arg1; arg2 ] -> equals com ctx r true arg1 arg2 |> Some
29812981
| "Compare", [ arg1; arg2 ] -> compare com ctx r arg1 arg2 |> Some
2982+
| "NonNull", [ arg ] -> arg |> Some
29822983
| _ -> None
29832984

29842985
let enums (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) =

tests/Js/Main/Fable.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
<Compile Include="TimeOnlyTests.fs" />
8282
<Compile Include="TimeSpanTests.fs" />
8383
<Compile Include="ListCollectorTests.fs" />
84+
<Compile Include="Nullness.fs" />
8485
<Compile Include="Main.fs" />
8586
</ItemGroup>
8687

tests/Js/Main/Nullness.fs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
module Fable.Tests.Nullness
2+
3+
open Util.Testing
4+
open Fable.Core.JsInterop
5+
6+
type ABNull =
7+
| A
8+
| B of (string | null)
9+
10+
let tests =
11+
testList
12+
"Nullness"
13+
[
14+
testCase
15+
"nullArgCheck"
16+
(fun () ->
17+
let ex =
18+
try
19+
nullArgCheck "arg" null
20+
with e ->
21+
e
22+
23+
equal "Value cannot be null. (Parameter 'arg')" ex.Message
24+
)
25+
26+
testCase "Null active pattern works"
27+
<| fun () ->
28+
let getLength abnull =
29+
match abnull with
30+
| A -> 0
31+
| B Null -> 0
32+
| B(NonNull s) -> s.Length // `s` is derived to be `string`
33+
34+
equal (getLength A) 0
35+
equal (getLength (B null)) 0
36+
37+
testCase "NonNull active pattern works"
38+
<| fun () ->
39+
let getLength abnull =
40+
match abnull with
41+
| A -> 0
42+
| B Null -> 0
43+
| B(NonNull s) -> s.Length // `s` is derived to be `string`
44+
45+
equal (getLength (B "hello")) 5
46+
47+
testCase "works with generics"
48+
<| fun _ ->
49+
// Generic code, note 'T must be constrained to be a reference type
50+
let findOrNull (index: int) (list: 'T list) : 'T | null when 'T: not struct =
51+
match List.tryItem index list with
52+
| Some item -> item
53+
| None -> null
54+
55+
equal (findOrNull 1 [ "a"; "b"; "c" ]) "b"
56+
equal (findOrNull 3 [ "a"; "b"; "c" ]) null
57+
58+
#if FABLE_COMPILER
59+
testCase "works for interop with undefined"
60+
<| fun () ->
61+
let maybeUndefined (value: string) : (string | null) = importMember "./js/nullness.js"
62+
63+
match maybeUndefined "ok" with
64+
| NonNull _ -> equal true true
65+
| Null -> equal true false
66+
67+
match maybeUndefined "foo" with
68+
| NonNull _ -> equal true false
69+
| Null -> equal true true
70+
71+
testCase "works for interop with null"
72+
<| fun () ->
73+
let maybeNull (value: string) : (string | null) = importMember "./js/nullness.js"
74+
75+
match maybeNull "ok" with
76+
| NonNull _ -> equal true true
77+
| Null -> equal true false
78+
79+
match maybeNull "foo" with
80+
| NonNull _ -> equal true false
81+
| Null -> equal true true
82+
#endif
83+
84+
testCase "Unchecked.nonNull works"
85+
<| fun () ->
86+
let toUpper (text: string | null) =
87+
(Unchecked.nonNull text).ToUpperInvariant()
88+
89+
equal (toUpper "hello") "HELLO"
90+
]

tests/Js/Main/js/nullness.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export function maybeUndefined(value) {
2+
if (value === "ok") {
3+
return value;
4+
}
5+
else {
6+
return undefined;
7+
}
8+
}
9+
10+
export function maybeNull(value) {
11+
if (value === "ok") {
12+
return value;
13+
}
14+
else {
15+
return null;
16+
}
17+
}

0 commit comments

Comments
 (0)