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

(*
* 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