You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
11 KiB
261 lines
11 KiB
(*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*)
|
|
|
|
open! IStd
|
|
|
|
let log_issue ?proc_name ~issue_log ~loc ~severity ~nullsafe_extra issue_type error_message =
|
|
let extras =
|
|
Jsonbug_t.{nullsafe_extra= Some nullsafe_extra; cost_polynomial= None; cost_degree= None}
|
|
in
|
|
let proc_name = Option.value proc_name ~default:Procname.Linters_dummy_method in
|
|
let trace = [Errlog.make_trace_element 0 loc error_message []] in
|
|
Reporting.log_issue_external proc_name severity ~issue_log ~loc ~extras ~ltr:trace issue_type
|
|
error_message
|
|
|
|
|
|
(* If the issue is related to violation of nullability type system rules *)
|
|
let is_typing_rules_violation = function
|
|
| TypeErr.Condition_redundant _ | TypeErr.Over_annotation _ ->
|
|
(* Those are not nullability type system violations per se *)
|
|
false
|
|
| TypeErr.Inconsistent_subclass _
|
|
| TypeErr.Nullable_dereference _
|
|
| TypeErr.Field_not_initialized _
|
|
| TypeErr.Bad_assignment _ ->
|
|
true
|
|
|
|
|
|
(* Yes, if the issue is a) "type violation" issue b) reportable to the user in a given mode *)
|
|
let is_reportable_typing_rules_violation ~nullsafe_mode issue =
|
|
is_typing_rules_violation issue && TypeErr.is_reportable ~nullsafe_mode issue
|
|
|
|
|
|
let get_reportable_typing_rules_violations modes_with_issues =
|
|
List.filter modes_with_issues ~f:(fun (nullsafe_mode, issue) ->
|
|
is_reportable_typing_rules_violation ~nullsafe_mode issue )
|
|
|> List.map ~f:(fun (_, a) -> a)
|
|
|
|
|
|
let get_reportable_typing_rules_violations_for_mode ~nullsafe_mode issues =
|
|
List.map issues ~f:(fun issue -> (nullsafe_mode, issue)) |> get_reportable_typing_rules_violations
|
|
|
|
|
|
type meta_issue =
|
|
{ issue_type: IssueType.t
|
|
; description: string
|
|
; severity: Exceptions.severity
|
|
; meta_issue_info: Jsonbug_t.nullsafe_meta_issue_info }
|
|
|
|
let mode_to_json mode =
|
|
let open NullsafeMode in
|
|
match mode with
|
|
| Default ->
|
|
`Default
|
|
| Local Trust.All ->
|
|
`LocalTrustAll
|
|
| Local (Trust.Only trust_list) when Trust.is_trust_none trust_list ->
|
|
`LocalTrustNone
|
|
| Local (Trust.Only _) ->
|
|
`LocalTrustSome
|
|
| Strict ->
|
|
`Strict
|
|
|
|
|
|
let is_clean_in_mode nullsafe_mode all_issues =
|
|
get_reportable_typing_rules_violations_for_mode ~nullsafe_mode all_issues |> List.is_empty
|
|
|
|
|
|
(* Return the maximum mode where we still have zero issues, or None if no such mode exists.
|
|
*)
|
|
let calc_strictest_mode_with_zero_issues all_issues =
|
|
let modes_to_try = NullsafeMode.[Strict; Local Trust.none; Local Trust.All; Default] in
|
|
List.find modes_to_try ~f:(fun mode -> is_clean_in_mode mode all_issues)
|
|
|
|
|
|
(* The maximum strict mode this mode can be promoted to with still having zero issues, if exists *)
|
|
let calc_mode_to_promote_to curr_mode all_issues =
|
|
let open IOption.Let_syntax in
|
|
let* strictest_mode = calc_strictest_mode_with_zero_issues all_issues in
|
|
if NullsafeMode.is_stricter_than ~stricter:strictest_mode ~weaker:curr_mode then
|
|
Some strictest_mode
|
|
else None
|
|
|
|
|
|
(* analyze all issues for the current class (including all nested and anonymous classes recursively)
|
|
and classify them into one meta-issue.
|
|
*)
|
|
let make_meta_issue modes_and_issues top_level_class_mode top_level_class_name =
|
|
let currently_reportable_issues = get_reportable_typing_rules_violations modes_and_issues in
|
|
List.iter currently_reportable_issues ~f:(fun issue ->
|
|
Logging.debug Analysis Medium "Issue: %a@\n" TypeErr.pp_err_instance issue ) ;
|
|
let currently_reportable_issue_count = List.length currently_reportable_issues in
|
|
let all_issues = List.map modes_and_issues ~f:(fun (_, a) -> a) in
|
|
let mode_to_promote_to =
|
|
if currently_reportable_issue_count > 0 then
|
|
(* This is not an optimization - consider a class with a nested class that is in the stricter mode than top level mode,
|
|
and has issues in this mode, but not in the top level mode.
|
|
This class is already broken now, so mode to promote to should be None.
|
|
But [calc_mode_to_promote_to] can return some mode for this case, which would be wrong. *)
|
|
None
|
|
else calc_mode_to_promote_to top_level_class_mode all_issues
|
|
in
|
|
let meta_issue_info =
|
|
Jsonbug_t.
|
|
{ num_issues= currently_reportable_issue_count
|
|
; curr_nullsafe_mode= mode_to_json top_level_class_mode
|
|
; can_be_promoted_to= Option.map mode_to_promote_to ~f:mode_to_json }
|
|
in
|
|
let issue_type, description, severity =
|
|
if NullsafeMode.equal top_level_class_mode Default then
|
|
match mode_to_promote_to with
|
|
| Some mode_to_promote_to ->
|
|
let message =
|
|
Format.sprintf
|
|
"Congrats! `%s` is free of nullability issues. Mark it \
|
|
`@Nullsafe(Nullsafe.Mode.LOCAL)` to prevent regressions."
|
|
(JavaClassName.classname top_level_class_name)
|
|
in
|
|
(IssueType.eradicate_meta_class_can_be_nullsafe, message, Exceptions.Advice)
|
|
| None ->
|
|
(* This class can not be made @Nullsafe without extra work *)
|
|
let issue_count_to_make_nullsafe =
|
|
get_reportable_typing_rules_violations_for_mode
|
|
~nullsafe_mode:(NullsafeMode.Local NullsafeMode.Trust.All) all_issues
|
|
|> List.length
|
|
in
|
|
( IssueType.eradicate_meta_class_needs_improvement
|
|
, Format.asprintf "`%s` needs %d issues to be fixed in order to be marked @Nullsafe."
|
|
(JavaClassName.classname top_level_class_name)
|
|
issue_count_to_make_nullsafe
|
|
, Exceptions.Info )
|
|
else if currently_reportable_issue_count > 0 then
|
|
(* This class is @Nullsafe, but broken. This should not happen often if there is enforcement for
|
|
@Nullsafe mode error in the target codebase. *)
|
|
( IssueType.eradicate_meta_class_needs_improvement
|
|
, Format.asprintf
|
|
"@Nullsafe classes should have exactly zero nullability issues. `%s` has %d."
|
|
(JavaClassName.classname top_level_class_name)
|
|
currently_reportable_issue_count
|
|
, Exceptions.Info )
|
|
else
|
|
( IssueType.eradicate_meta_class_is_nullsafe
|
|
, Format.asprintf "Class %a is free of nullability issues." JavaClassName.pp
|
|
top_level_class_name
|
|
, Exceptions.Info )
|
|
in
|
|
{issue_type; description; severity; meta_issue_info}
|
|
|
|
|
|
let get_class_loc source_file Struct.{java_class_info} =
|
|
match java_class_info with
|
|
| Some {loc} ->
|
|
(* In rare cases location is not present, fall back to the first line of the file *)
|
|
Option.value loc ~default:Location.{file= source_file; line= 1; col= 0}
|
|
| None ->
|
|
Logging.die InternalError "java_class_info should be present for Java classes"
|
|
|
|
|
|
(* Meta issues are those related to null-safety of the class in general, not concrete nullability violations *)
|
|
let report_meta_issue_for_top_level_class tenv source_file class_name class_struct class_info
|
|
issue_log =
|
|
if Option.is_some (JavaClassName.get_outer_class_name class_name) then
|
|
(* We record meta-issues only for top-level classes *) issue_log
|
|
else
|
|
let current_mode = NullsafeMode.of_class tenv class_name in
|
|
(* For purposes of aggregation, we consider all nested summaries as belonging to this class *)
|
|
let class_names_and_summaries =
|
|
AggregatedSummaries.ClassInfo.get_recursive_summaries class_info
|
|
in
|
|
let class_loc = get_class_loc source_file class_struct in
|
|
let all_issues =
|
|
List.map class_names_and_summaries ~f:(fun (class_name, NullsafeSummary.{issues}) ->
|
|
let mode_for_class_name = NullsafeMode.of_class tenv class_name in
|
|
List.map issues ~f:(fun issue -> (mode_for_class_name, issue)) )
|
|
|> List.fold ~init:[] ~f:( @ )
|
|
in
|
|
let {issue_type; description; severity; meta_issue_info} =
|
|
make_meta_issue all_issues current_mode class_name
|
|
in
|
|
let package = JavaClassName.package class_name in
|
|
let class_name = JavaClassName.classname class_name in
|
|
let nullsafe_extra = Jsonbug_t.{class_name; package; meta_issue_info= Some meta_issue_info} in
|
|
log_issue ~issue_log ~loc:class_loc ~severity ~nullsafe_extra issue_type description
|
|
|
|
|
|
(* Optimization - if issues are disabled, don't bother analyzing them *)
|
|
let should_analyze_meta_issues () =
|
|
(not Config.filtering) || IssueType.eradicate_meta_class_can_be_nullsafe.enabled
|
|
|| IssueType.eradicate_meta_class_needs_improvement.enabled
|
|
|| IssueType.eradicate_meta_class_is_nullsafe.enabled
|
|
|
|
|
|
let analyze_meta_issue_for_top_level_class tenv source_file class_name class_struct class_info
|
|
issue_log =
|
|
if should_analyze_meta_issues () then
|
|
report_meta_issue_for_top_level_class tenv source_file class_name class_struct class_info
|
|
issue_log
|
|
else issue_log
|
|
|
|
|
|
let analyze_nullsafe_annotations tenv source_file class_name class_struct issue_log =
|
|
let loc = get_class_loc source_file class_struct in
|
|
let nullsafe_extra =
|
|
let package = JavaClassName.package class_name in
|
|
let class_name = JavaClassName.classname class_name in
|
|
Jsonbug_t.{class_name; package; meta_issue_info= None}
|
|
in
|
|
match NullsafeMode.check_problematic_class_annotation tenv class_name with
|
|
| Ok () ->
|
|
issue_log
|
|
| Error NullsafeMode.RedundantNestedClassAnnotation ->
|
|
let description =
|
|
Format.sprintf
|
|
"`%s`: the same @Nullsafe mode is already specified in the outer class, so this \
|
|
annotation can be removed."
|
|
(JavaClassName.classname class_name)
|
|
in
|
|
log_issue ~issue_log ~loc ~nullsafe_extra ~severity:Exceptions.Advice
|
|
IssueType.eradicate_redundant_nested_class_annotation description
|
|
| Error (NullsafeMode.NestedModeIsWeaker (ExtraTrustClass wrongly_trusted_classes)) ->
|
|
(* The list can not be empty *)
|
|
let example_of_wrongly_trusted_class = List.nth_exn wrongly_trusted_classes 0 in
|
|
let description =
|
|
Format.sprintf
|
|
"Nested classes cannot add classes to trust list if they are not in the outer class \
|
|
trust list. Remove `%s` from trust list."
|
|
(JavaClassName.classname example_of_wrongly_trusted_class)
|
|
in
|
|
log_issue ~issue_log ~loc ~nullsafe_extra ~severity:Exceptions.Warning
|
|
IssueType.eradicate_bad_nested_class_annotation description
|
|
| Error (NullsafeMode.NestedModeIsWeaker Other) ->
|
|
let description =
|
|
Format.sprintf
|
|
"`%s`: nested classes are disallowed to weaken @Nullsafe mode specified in the outer \
|
|
class. This annotation will be ignored."
|
|
(JavaClassName.classname class_name)
|
|
in
|
|
log_issue ~issue_log ~loc ~nullsafe_extra ~severity:Exceptions.Warning
|
|
IssueType.eradicate_bad_nested_class_annotation description
|
|
|
|
|
|
let analyze_class_impl tenv source_file class_name class_struct class_info issue_log =
|
|
issue_log
|
|
|> analyze_meta_issue_for_top_level_class tenv source_file class_name class_struct class_info
|
|
|> analyze_nullsafe_annotations tenv source_file class_name class_struct
|
|
|
|
|
|
let analyze_class tenv source_file class_info issue_log =
|
|
let class_name = AggregatedSummaries.ClassInfo.get_class_name class_info in
|
|
match Tenv.lookup tenv (Typ.JavaClass class_name) with
|
|
| Some class_struct ->
|
|
analyze_class_impl tenv source_file class_name class_struct class_info issue_log
|
|
| None ->
|
|
Logging.debug Analysis Medium
|
|
"%a: could not load class info in environment: skipping class analysis@\n" JavaClassName.pp
|
|
class_name ;
|
|
issue_log
|