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.

300 lines
14 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
type violation = {lhs: AnnotatedNullability.t; rhs: InferredNullability.t} [@@deriving compare]
module ProvisionalViolation = struct
type t =
{ fix_annotation: ProvisionalAnnotation.t option
; offending_annotations: ProvisionalAnnotation.t list }
let offending_annotations {offending_annotations} = offending_annotations
let fix_annotation {fix_annotation} = fix_annotation
let from {lhs; rhs} =
let offending_annotations = InferredNullability.get_provisional_annotations rhs in
if List.is_empty offending_annotations then None
else
let fix_annotation =
match lhs with
| AnnotatedNullability.ProvisionallyNullable annotation ->
Some annotation
| _ ->
None
in
Some {offending_annotations; fix_annotation}
end
module ReportableViolation = struct
type t = {nullsafe_mode: NullsafeMode.t; violation: violation}
type assignment_type =
| PassingParamToFunction of function_info
| AssigningToField of Fieldname.t
| ReturningFromFunction of Procname.Java.t
[@@deriving compare]
and function_info =
{ param_signature: AnnotatedSignature.param_signature
; actual_param_expression: string
; param_index: int
; annotated_signature: AnnotatedSignature.t
; procname: Procname.Java.t }
let from nullsafe_mode ({lhs; rhs} as violation) =
let falls_under_optimistic_third_party =
Config.nullsafe_optimistic_third_party_in_default_mode
&& NullsafeMode.equal nullsafe_mode Default
(* Treat third party params as if they were [@Nullable] *)
&& Nullability.equal (AnnotatedNullability.get_nullability lhs) ThirdPartyNonnull
in
let is_non_reportable =
falls_under_optimistic_third_party
|| (* In certain modes, we trust rhs to be non-nullable and don't report violation *)
Nullability.is_considered_nonnull ~nullsafe_mode (InferredNullability.get_nullability rhs)
in
if is_non_reportable then None else Some {nullsafe_mode; violation}
let get_origin_opt assignment_type origin =
let should_show_origin =
match assignment_type with
| PassingParamToFunction {actual_param_expression} ->
not
(ErrorRenderingUtils.is_object_nullability_self_explanatory
~object_expression:actual_param_expression origin)
| AssigningToField _ | ReturningFromFunction _ ->
true
in
if should_show_origin then Some origin else None
let pp_param_name fmt mangled =
let name = Mangled.to_string mangled in
if String.is_substring name ~substring:"_arg_" then
(* The real name was not fetched for whatever reason, this is an autogenerated name *)
Format.fprintf fmt ""
else Format.fprintf fmt "(%a)" MarkupFormatter.pp_monospaced name
(* A slight adapter over [NullsafeIssue.make]: the same signature but additionally accepts an alternative method *)
let make_issue_with_recommendation ~description ~rhs_origin ~issue_type ~loc ~severity ~field_name
=
(* If there is an alternative method to propose, tell about it at the end of the description *)
let alternative_method =
ErrorRenderingUtils.find_alternative_nonnull_method_description rhs_origin
in
let alternative_recommendation =
Option.value_map alternative_method
~f:
(Format.asprintf " If you don't expect null, use %a instead."
MarkupFormatter.pp_monospaced)
~default:""
in
let full_description = Format.sprintf "%s%s" description alternative_recommendation in
let nullable_methods =
match rhs_origin with TypeOrigin.MethodCall origin -> [origin] | _ -> []
in
NullsafeIssue.make ~description:full_description ~issue_type ~loc ~severity ~field_name
|> NullsafeIssue.with_nullable_methods nullable_methods
let mk_issue_for_bad_param_passed
{annotated_signature; param_signature; actual_param_expression; param_index; procname}
~param_nullability_kind ~nullability_evidence
~(make_issue_factory : description:string -> issue_type:IssueType.t -> NullsafeIssue.t) =
let nullability_evidence_as_suffix =
Option.value_map nullability_evidence ~f:(fun evidence -> ": " ^ evidence) ~default:""
in
let annotated_param_nullability = param_signature.param_annotated_type.nullability in
let module MF = MarkupFormatter in
let argument_description =
if String.equal actual_param_expression "null" then "is `null`"
else
let nullability_descr =
match param_nullability_kind with
| ErrorRenderingUtils.UserFriendlyNullable.Null ->
"`null`"
| ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
"nullable"
in
Format.asprintf "%a is %s" MF.pp_monospaced actual_param_expression nullability_descr
in
let issue_type = IssueType.eradicate_parameter_not_nullable in
let issue =
match AnnotatedNullability.get_nullability annotated_param_nullability with
| Nullability.Null ->
Logging.die Logging.InternalError "Unexpected param nullability: Null"
| Nullability.Nullable ->
Logging.die Logging.InternalError "Passing anything to a nullable param should be allowed"
| Nullability.ThirdPartyNonnull ->
(* This is a special case. While for FB codebase we can assume "not annotated hence not nullable" rule for all_whitelisted signatures,
This is not the case for third party functions, which can have different conventions,
So we can not just say "param is declared as non-nullable" like we say for FB-internal or modelled case:
param can be nullable according to API but it was just not annotated.
So we phrase it differently to remain truthful, but as specific as possible.
*)
let suggested_third_party_sig_file =
ThirdPartyAnnotationInfo.lookup_related_sig_file_for_proc
(ThirdPartyAnnotationGlobalRepo.get_repo ())
procname
in
let where_to_add_signature =
Option.value_map suggested_third_party_sig_file
~f:(fun sig_file_name ->
ThirdPartyAnnotationGlobalRepo.get_user_friendly_third_party_sig_file_name
~filename:sig_file_name )
(* this can happen when third party is registered in a deprecated way (not in third party repository) *)
~default:"the third party signature storage"
in
let procname_str = Procname.Java.to_simplified_string ~withclass:true procname in
let description =
Format.asprintf
"Third-party %a is missing a signature that would allow passing a nullable to param \
#%d%a. Actual argument %s%s. Consider adding the correct signature of %a to %s."
MF.pp_monospaced procname_str
(param_index + 1) (* human-readable param number is indexed from 1 *)
pp_param_name param_signature.mangled argument_description
nullability_evidence_as_suffix MF.pp_monospaced procname_str where_to_add_signature
in
make_issue_factory ~description ~issue_type
|> NullsafeIssue.with_third_party_dependent_methods [(procname, annotated_signature)]
(* Equivalent to non-null from user point of view *)
| Nullability.ProvisionallyNullable
| Nullability.LocallyCheckedNonnull
| Nullability.LocallyTrustedNonnull
| Nullability.UncheckedNonnull
| Nullability.StrictNonnull ->
let nonnull_evidence =
match annotated_signature.kind with
| FirstParty | ThirdParty Unregistered ->
""
| ThirdParty ModelledInternally ->
" (according to nullsafe internal models)"
| ThirdParty (InThirdPartyRepo {filename; line_number}) ->
Format.sprintf " (see %s at line %d)"
(ThirdPartyAnnotationGlobalRepo.get_user_friendly_third_party_sig_file_name
~filename)
line_number
in
let description =
Format.asprintf "%a: parameter #%d%a is declared non-nullable%s but the argument %s%s."
MF.pp_monospaced
(Procname.Java.to_simplified_string ~withclass:true procname)
(param_index + 1) (* human-readable param number is indexed from 1 *)
pp_param_name param_signature.mangled nonnull_evidence argument_description
nullability_evidence_as_suffix
in
make_issue_factory ~description ~issue_type
in
issue |> NullsafeIssue.with_parameter_not_nullable_info ~param_index ~proc_name:procname
let field_name_of_assignment_type = function
| AssigningToField field_name ->
Some field_name
| PassingParamToFunction _ | ReturningFromFunction _ ->
None
let mk_nullsafe_issue_for_explicitly_nullable_values ~assignment_type ~rhs_origin ~nullsafe_mode
~explicit_rhs_nullable_kind ~assignment_location =
let nullability_evidence =
get_origin_opt assignment_type rhs_origin
|> Option.bind ~f:(fun origin -> TypeOrigin.get_description origin)
in
let nullability_evidence_as_suffix =
Option.value_map nullability_evidence ~f:(fun evidence -> ": " ^ evidence) ~default:""
in
(* A "factory" - a high-order function for creating the nullsafe issue: fill in what is already known at this point.
The rest to be filled by the client *)
let make_issue_factory =
make_issue_with_recommendation ~rhs_origin
~severity:(NullsafeMode.severity nullsafe_mode)
~loc:assignment_location
~field_name:(field_name_of_assignment_type assignment_type)
in
match assignment_type with
| PassingParamToFunction function_info ->
mk_issue_for_bad_param_passed function_info ~nullability_evidence
~param_nullability_kind:explicit_rhs_nullable_kind ~make_issue_factory
| AssigningToField field_name ->
let rhs_description =
match explicit_rhs_nullable_kind with
| ErrorRenderingUtils.UserFriendlyNullable.Null ->
"`null`"
| ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
"a nullable"
in
let description =
Format.asprintf "%a is declared non-nullable but is assigned %s%s."
MarkupFormatter.pp_monospaced
(Fieldname.get_field_name field_name)
rhs_description nullability_evidence_as_suffix
in
make_issue_factory ~description ~issue_type:IssueType.eradicate_field_not_nullable
| ReturningFromFunction function_proc_name ->
let return_description =
match explicit_rhs_nullable_kind with
| ErrorRenderingUtils.UserFriendlyNullable.Null ->
(* Return `null` in all_whitelisted branches *)
"`null`"
| ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
"a nullable value"
in
let description =
Format.asprintf "%a: return type is declared non-nullable but the method returns %s%s."
MarkupFormatter.pp_monospaced
(Procname.Java.to_simplified_string ~withclass:false function_proc_name)
return_description nullability_evidence_as_suffix
in
make_issue_factory ~description ~issue_type:IssueType.eradicate_return_not_nullable
let make_nullsafe_issue ~assignment_location assignment_type {nullsafe_mode; violation= {rhs}} =
let rhs_origin = InferredNullability.get_simple_origin rhs in
let user_friendly_nullable =
ErrorRenderingUtils.UserFriendlyNullable.from_nullability
(InferredNullability.get_nullability rhs)
|> IOption.if_none_eval ~f:(fun () ->
Logging.die InternalError
"get_description:: Assignment violation should not be possible for non-nullable \
values on right hand side" )
in
match user_friendly_nullable with
| ErrorRenderingUtils.UserFriendlyNullable.UntrustedNonnull untrusted_kind ->
(* Attempt to assigning a value which is not explictly declared as nullable,
but still can not be trusted in this particular mode.
*)
ErrorRenderingUtils.mk_nullsafe_issue_for_untrusted_values ~nullsafe_mode ~untrusted_kind
~bad_usage_location:assignment_location rhs_origin
| ErrorRenderingUtils.UserFriendlyNullable.ExplainablyNullable explicit_kind ->
(* Attempt to assigning a value that can be explained to the user as nullable. *)
mk_nullsafe_issue_for_explicitly_nullable_values ~assignment_type ~rhs_origin ~nullsafe_mode
~explicit_rhs_nullable_kind:explicit_kind ~assignment_location
end
let check ~lhs ~rhs =
match (lhs, InferredNullability.get_nullability rhs) with
| AnnotatedNullability.ProvisionallyNullable _, Nullability.ProvisionallyNullable ->
(* This is a special case. Assignment of something that comes from provisionally nullable annotation to something that
is annotated as provisionally nullable is a (provisional) violation.
(With an exception when it is an assignment to the same annotation e.g. in recursion calls;
but such exceptions are non-essential for the purposes of calculation of the annotation graph.
)
*)
Error {lhs; rhs}
| _ ->
let is_subtype =
Nullability.is_subtype
~supertype:(AnnotatedNullability.get_nullability lhs)
~subtype:(InferredNullability.get_nullability rhs)
in
Result.ok_if_true is_subtype ~error:{lhs; rhs}