Skip to content

Commit aa636dc

Browse files
committed
[nullsafety] Non-nullable check warnings
1 parent 5497f50 commit aa636dc

File tree

8 files changed

+157
-26
lines changed

8 files changed

+157
-26
lines changed

src-json/warning.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@
125125
"parent": "WTyper",
126126
"enabled": false
127127
},
128+
{
129+
"name": "WRedundantNullCheck",
130+
"doc": "Value can't be null, so comparison with null is excessive",
131+
"parent": "WTyper"
132+
},
128133
{
129134
"name": "WHxb",
130135
"doc": "Hxb (either --hxb output or haxe compiler cache) related warnings"

src/context/common.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class compiler_callbacks = object(self)
7676
method add_after_generation (f : unit -> unit) : unit =
7777
after_generation := f :: !after_generation
7878

79-
method add_null_safety_report (f : (string*pos) list -> unit) : unit =
79+
method add_null_safety_report (f : (WarningList.warning option*string*pos) list -> unit) : unit =
8080
null_safety_report <- f :: null_safety_report
8181

8282
method run handle_error r =
@@ -1110,4 +1110,4 @@ let make_unforced_lazy t_proc f where =
11101110
| Error.Error e ->
11111111
raise (Error.Fatal_error e)
11121112
);
1113-
r
1113+
r

src/macro/macroApi.ml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,9 +2375,10 @@ let macro_api ccom get_api =
23752375
);
23762376
"on_null_safety_report", vfun1 (fun f ->
23772377
let f = prepare_callback f 1 in
2378-
(ccom()).callbacks#add_null_safety_report (fun (errors:(string*pos) list) ->
2379-
let encode_item (msg,pos) =
2380-
encode_obj [("msg", encode_string msg); ("pos", encode_pos pos)]
2378+
(ccom()).callbacks#add_null_safety_report (fun (errors:(WarningList.warning option*string*pos) list) ->
2379+
let encode_item (wtype,msg,pos) =
2380+
let wtype = match wtype with | Some _ -> "warning" | None -> "error" in
2381+
encode_obj [("type", encode_string wtype); ("msg", encode_string msg); ("pos", encode_pos pos)]
23812382
in
23822383
ignore(f [encode_array (List.map encode_item errors)])
23832384
);

src/typing/nullSafety.ml

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ open Type
55
type safety_message = {
66
sm_msg : string;
77
sm_pos : pos;
8+
sm_type : WarningList.warning option
89
}
910

1011
type safety_report = {
1112
mutable sr_errors : safety_message list;
13+
mutable sr_warnings: safety_message list;
1214
}
1315

1416
let add_error report msg pos =
15-
let error = { sm_msg = ("Null safety: " ^ msg); sm_pos = pos; } in
17+
let error = { sm_type = None; sm_msg = ("Null safety: " ^ msg); sm_pos = pos; } in
1618
if not (List.mem error report.sr_errors) then
17-
report.sr_errors <- error :: report.sr_errors;
19+
report.sr_errors <- error :: report.sr_errors;;
20+
21+
let add_warning report wtype msg pos =
22+
let warning = { sm_type = Some wtype; sm_msg = ("Null safety: " ^ msg); sm_pos = pos; } in
23+
if not (List.mem warning report.sr_warnings) then
24+
report.sr_warnings <- warning :: report.sr_warnings;
1825

1926
type scope_type =
2027
| STNormal
@@ -447,7 +454,7 @@ let rec contains_safe_meta metadata =
447454
let safety_enabled meta =
448455
(contains_safe_meta meta) && not (contains_unsafe_meta meta)
449456

450-
let safety_mode (metadata:Ast.metadata) =
457+
let get_safety_mode (metadata:Ast.metadata) =
451458
let rec traverse mode meta =
452459
match mode, meta with
453460
| Some SMOff, _
@@ -1053,7 +1060,6 @@ class expr_checker mode immediate_execution report =
10531060
val mutable in_closure = false
10541061
(* if this flag is `true` then spotted errors and warnings will not be reported *)
10551062
val mutable is_pretending = false
1056-
(* val mutable cnt = 0 *)
10571063
(**
10581064
Get safety mode for this expression checker
10591065
*)
@@ -1072,6 +1078,33 @@ class expr_checker mode immediate_execution report =
10721078
in
10731079
add_error report msg (get_first_valid_pos positions)
10741080
end
1081+
(**
1082+
Register a warning
1083+
*)
1084+
method warning wtype msg (positions:Globals.pos list) =
1085+
if not is_pretending then begin
1086+
let rec get_first_valid_pos positions =
1087+
match positions with
1088+
| [] -> null_pos
1089+
| p :: rest ->
1090+
if p <> null_pos then p
1091+
else get_first_valid_pos rest
1092+
in
1093+
add_warning report wtype msg (get_first_valid_pos positions)
1094+
end
1095+
1096+
method private check_binop_redundant_null_checks e =
1097+
match e.eexpr with
1098+
| TBinop ((OpEq | OpNotEq), { eexpr = TConst TNull }, expr)
1099+
| TBinop ((OpEq | OpNotEq), expr, { eexpr = TConst TNull })
1100+
| TBinop(OpAssignOp OpNullCoal, expr, _)
1101+
| TBinop (OpNullCoal, expr, _) ->
1102+
if not (is_nullable_type ~dynamic_is_nullable:true expr.etype) then
1103+
self#warning
1104+
WRedundantNullCheck
1105+
("The operand type is not nullable, so null-check should be redundant.")
1106+
[expr.epos; e.epos];
1107+
| _ -> ()
10751108
(**
10761109
Check if `e` is nullable even if the type is reported not-nullable.
10771110
Haxe type system lies sometimes.
@@ -1180,7 +1213,9 @@ class expr_checker mode immediate_execution report =
11801213
| TConst _ -> ()
11811214
| TLocal _ -> ()
11821215
| TArray (arr, idx) -> self#check_array_access arr idx e.epos
1183-
| TBinop (op, left_expr, right_expr) -> self#check_binop op left_expr right_expr e.epos
1216+
| TBinop (op, left_expr, right_expr) ->
1217+
self#check_binop_redundant_null_checks e;
1218+
self#check_binop op left_expr right_expr e.epos
11841219
| TField (target, access) -> self#check_field target access e.epos
11851220
| TTypeExpr _ -> ()
11861221
| TParenthesis e -> self#check_expr e
@@ -1539,7 +1574,7 @@ class class_checker cls immediate_execution report (main_expr : texpr option) =
15391574
object (self)
15401575
val is_safe_class = (safety_enabled cls_meta)
15411576
val mutable checker = new expr_checker SMLoose immediate_execution report
1542-
val mutable mode = None
1577+
val mutable mode : safety_mode option = None
15431578
(**
15441579
Entry point for checking a class
15451580
*)
@@ -1549,7 +1584,7 @@ class class_checker cls immediate_execution report (main_expr : texpr option) =
15491584
self#check_var_fields;
15501585
let check_field is_static f = if not (has_class_field_flag f CfPostProcessed) then begin
15511586
validate_safety_meta report f.cf_meta;
1552-
match (safety_mode (cls_meta @ f.cf_meta)) with
1587+
match (get_safety_mode (cls_meta @ f.cf_meta)) with
15531588
| SMOff -> ()
15541589
| mode ->
15551590
(match f.cf_expr with
@@ -1560,7 +1595,7 @@ class class_checker cls immediate_execution report (main_expr : texpr option) =
15601595
self#check_accessors is_static f
15611596
end in
15621597
if is_safe_class then
1563-
Option.may ((self#get_checker (safety_mode cls_meta))#check_root_expr) (TClass.get_cl_init cls);
1598+
Option.may ((self#get_checker (get_safety_mode cls_meta))#check_root_expr) (TClass.get_cl_init cls);
15641599
Option.may (check_field false) cls.cl_constructor;
15651600
List.iter (check_field false) cls.cl_ordered_fields;
15661601
List.iter (check_field true) cls.cl_ordered_statics;
@@ -1601,7 +1636,7 @@ class class_checker cls immediate_execution report (main_expr : texpr option) =
16011636
match mode with
16021637
| Some mode -> mode
16031638
| None ->
1604-
let m = safety_mode cls_meta in
1639+
let m = get_safety_mode cls_meta in
16051640
mode <- Some m;
16061641
m
16071642
(**
@@ -1784,7 +1819,10 @@ class class_checker cls immediate_execution report (main_expr : texpr option) =
17841819
*)
17851820
let run (com:Common.context) (types:module_type list) =
17861821
let report = Timer.time com.timer_ctx ["null safety"] (fun () ->
1787-
let report = { sr_errors = [] } in
1822+
let report = {
1823+
sr_errors = [];
1824+
sr_warnings = [];
1825+
} in
17881826
let immediate_execution = new immediate_execution in
17891827
let traverse module_type =
17901828
match module_type with
@@ -1798,11 +1836,21 @@ let run (com:Common.context) (types:module_type list) =
17981836
) () in
17991837
match com.callbacks#get_null_safety_report with
18001838
| [] ->
1801-
List.iter (fun err -> Common.display_error com err.sm_msg err.sm_pos) (List.rev report.sr_errors)
1839+
List.iter (fun warn ->
1840+
com.warning (Option.get warn.sm_type) [] warn.sm_msg warn.sm_pos
1841+
) (List.rev report.sr_warnings);
1842+
1843+
List.iter (fun err ->
1844+
Common.display_error com err.sm_msg err.sm_pos
1845+
) (List.rev report.sr_errors)
18021846
| callbacks ->
1847+
let warnings =
1848+
List.map (fun warn -> (warn.sm_type, warn.sm_msg, warn.sm_pos)) report.sr_warnings
1849+
in
18031850
let errors =
1804-
List.map (fun err -> (err.sm_msg, err.sm_pos)) report.sr_errors
1851+
List.map (fun err -> (err.sm_type, err.sm_msg, err.sm_pos)) report.sr_errors
18051852
in
1806-
List.iter (fun fn -> fn errors) callbacks
1853+
let all = warnings @ errors in
1854+
List.iter (fun fn -> fn all) callbacks
18071855

18081856
;;

tests/nullsafety/src/Validator.hx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22
import haxe.macro.Context;
33
import haxe.macro.Expr;
44

5-
typedef SafetyMessage = {msg:String, pos:Position}
6-
typedef ExpectedMessage = {symbol:String, pos:Position}
5+
typedef SafetyMessage = {type:String, msg:String, pos:Position}
6+
typedef ExpectedMessage = {type:String, symbol:String, pos:Position}
77
#end
88

99
class Validator {
1010
#if macro
1111
static var expectedErrors:Array<ExpectedMessage> = [];
12+
static var expectedWarnings:Array<ExpectedMessage> = [];
1213

1314
static dynamic function onNullSafetyReport(callback:(errors:Array<SafetyMessage>)->Void):Void {
1415
}
1516

1617
static public function register() {
1718
expectedErrors = [];
19+
expectedWarnings = [];
1820
onNullSafetyReport = @:privateAccess Context.load("on_null_safety_report", 1);
1921
onNullSafetyReport(validate);
2022
}
@@ -25,7 +27,13 @@ class Validator {
2527
if(meta.name == ':shouldFail') {
2628
var fieldPosInfos = Context.getPosInfos(field.pos);
2729
fieldPosInfos.min = Context.getPosInfos(meta.pos).max + 1;
28-
expectedErrors.push({symbol: field.name, pos:Context.makePosition(fieldPosInfos)});
30+
expectedErrors.push({type: "error", symbol: field.name, pos:Context.makePosition(fieldPosInfos)});
31+
break;
32+
}
33+
if(meta.name == ':shouldWarn') {
34+
var fieldPosInfos = Context.getPosInfos(field.pos);
35+
fieldPosInfos.min = Context.getPosInfos(meta.pos).max + 1;
36+
expectedWarnings.push({type: "warning", symbol: field.name, pos:Context.makePosition(fieldPosInfos)});
2937
break;
3038
}
3139
}
@@ -34,7 +42,7 @@ class Validator {
3442
}
3543

3644
static function validate(errors:Array<SafetyMessage>) {
37-
var errors = check(expectedErrors.copy(), errors.copy());
45+
var errors = check(expectedErrors.concat(expectedWarnings), errors.copy());
3846
if(errors.ok) {
3947
Sys.println('${errors.passed} expected errors spotted');
4048
Sys.println('Compile-time tests passed.');
@@ -50,6 +58,7 @@ class Validator {
5058
var actualEvent = actual[i];
5159
var wasExpected = false;
5260
for(expectedEvent in expected) {
61+
if (expectedEvent.type != actualEvent.type) continue;
5362
if(posContains(expectedEvent.pos, actualEvent.pos)) {
5463
expected.remove(expectedEvent);
5564
wasExpected = true;
@@ -85,7 +94,12 @@ class Validator {
8594
#end
8695

8796
macro static public function shouldFail(expr:Expr):Expr {
88-
expectedErrors.push({symbol:Context.getLocalMethod(), pos:expr.pos});
97+
expectedErrors.push({type: "error", symbol:Context.getLocalMethod(), pos:expr.pos});
98+
return expr;
99+
}
100+
101+
macro static public function shouldWarn(expr:Expr):Expr {
102+
expectedWarnings.push({type: "warning", symbol:Context.getLocalMethod(), pos:expr.pos});
89103
return expr;
90104
}
91-
}
105+
}

tests/nullsafety/src/cases/TestLoose.hx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cases;
22

33
import Validator.shouldFail;
4+
import Validator.shouldWarn;
45

56
typedef NotNullAnon = {
67
a:String
@@ -133,7 +134,7 @@ class TestLoose {
133134
}
134135

135136
static function nullCoal_returnNull_shouldPass(token:{children:Array<Int>}):Null<Bool> {
136-
final children = token.children ?? return null;
137+
final children = shouldWarn(token.children ?? return null);
137138
var i = children.length;
138139
return null;
139140
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cases;
2+
3+
import Validator.shouldWarn;
4+
import Validator.shouldFail;
5+
6+
typedef Data = {
7+
var foo:String;
8+
}
9+
10+
class TestNonNullable {
11+
static function main() {
12+
final foo = 0;
13+
if (shouldWarn(foo) == null) {}
14+
15+
final dyn:Dynamic = null;
16+
if (dyn == null) {}
17+
18+
final dyn:Any = 1;
19+
if (shouldWarn(dyn) == null) {}
20+
21+
var data:Data = haxe.Json.parse("{}");
22+
data.foo.length;
23+
24+
switch shouldWarn(data.foo) {
25+
case null if (shouldWarn(data.foo) == null):
26+
final v = shouldWarn(data.foo) == null;
27+
}
28+
29+
final v = shouldWarn(data.foo) == null;
30+
shouldWarn(data.foo) != null && true;
31+
true && shouldWarn(data.foo) != null;
32+
data.foo != null || true;
33+
true || shouldWarn(data.foo) != null;
34+
35+
throw shouldWarn(data.foo) == null;
36+
37+
function foo():Bool {
38+
return shouldWarn(data.foo) == null;
39+
}
40+
41+
while (shouldWarn(data.foo) == null) {}
42+
43+
shouldWarn(data.foo) ??= "";
44+
final foo = shouldWarn(data.foo ?? "");
45+
if (null == shouldWarn(data.foo)) {
46+
trace(1);
47+
}
48+
if (shouldWarn(data.foo) == null) {
49+
data.foo = "default";
50+
}
51+
}
52+
}
53+
54+
@:build(Validator.checkFields())
55+
class BasicErrors {
56+
@:shouldFail static var foo2:Int;
57+
public function new() {
58+
shouldFail(var foo:Int = null);
59+
}
60+
}

tests/nullsafety/test.hxml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ cases.TestStrictThreaded
55
cases.TestLoose
66
cases.TestSafeFieldInUnsafeClass
77
cases.TestAbstract
8+
cases.TestNonNullable
89

910
--macro nullSafety('cases.TestLoose', Loose)
1011
--macro nullSafety('cases.TestStrict', Strict)
1112
--macro nullSafety('cases.TestStrictThreaded', StrictThreaded)
12-
--macro Validator.register()
13+
--macro nullSafety('cases.TestNonNullable', Loose)
14+
--macro Validator.register()

0 commit comments

Comments
 (0)