[sql] let sqlite handle the attributes update logic

Summary:
This adds more structure to the SQL schema backing attributes. With that, we
can transfer the logic for updating attributes in SQLite, instead of doing
optimistic concurrency in the client.

Reviewed By: jberdine

Differential Revision: D5891038

fbshipit-source-id: 6577ba2
master
Jules Villard 7 years ago committed by Facebook Github Bot
parent b00451d23e
commit ee1b688e6d

@ -3,6 +3,7 @@ B _build/default/atd
B _build/default/istd
PKG ANSITerminal
PKG atdgen
PKG base
PKG cmdliner
PKG core
PKG javalib

@ -11,84 +11,109 @@ module L = Logging
type attributes_kind = ProcUndefined | ProcObjCAccessor | ProcDefined [@@deriving compare]
let least_relevant_up_to_proc_kind_exclusive = function
| ProcUndefined
-> []
| ProcObjCAccessor
-> [ProcUndefined]
| ProcDefined
-> [ProcUndefined; ProcObjCAccessor]
let most_relevant_down_to_proc_kind_inclusive = function
| ProcUndefined
-> [ProcDefined; ProcObjCAccessor; ProcUndefined]
| ProcObjCAccessor
-> [ProcDefined; ProcObjCAccessor]
| ProcDefined
-> [ProcDefined]
let int64_of_attributes_kind =
(* only allocate this once *)
let int64_two = Int64.of_int 2 in
function ProcUndefined -> Int64.zero | ProcObjCAccessor -> Int64.one | ProcDefined -> int64_two
let proc_kind_of_attr (proc_attributes: ProcAttributes.t) =
if proc_attributes.is_defined then ProcDefined
else if Option.is_some proc_attributes.objc_accessor then ProcObjCAccessor
else ProcUndefined
let should_override_attr attr1 attr2 =
(* use the source file to be more deterministic in case the same procedure name is defined in several files *)
[%compare : attributes_kind * SourceFile.t]
(proc_kind_of_attr attr1, attr1.ProcAttributes.loc.file)
(proc_kind_of_attr attr2, attr2.ProcAttributes.loc.file)
> 0
module type Data = sig
val of_pname : Typ.Procname.t -> Sqlite3.Data.t
module Table = struct
type key = string
val of_source_file : SourceFile.t -> Sqlite3.Data.t
type value = ProcAttributes.t
val of_proc_attr : ProcAttributes.t -> Sqlite3.Data.t
let table = ResultsDir.attributes_table
val to_proc_attr : Sqlite3.Data.t -> ProcAttributes.t
end
module Store = KeyValue.Make (Table)
module Data : Data = struct
let pname_to_key = Base.Hashtbl.create (module Typ.Procname) ()
let string_of_pkind = function
| ProcUndefined
-> "U"
| ProcObjCAccessor
-> "O"
| ProcDefined
-> "D"
let of_pname pname =
let default () = Sqlite3.Data.TEXT (Typ.Procname.to_filename pname) in
Base.Hashtbl.find_or_add pname_to_key pname ~default
module KeyHashtbl = Caml.Hashtbl.Make (struct
type t = Typ.Procname.t * attributes_kind
let of_source_file file = Sqlite3.Data.TEXT (SourceFile.to_string file)
let equal = [%compare.equal : Typ.Procname.t * attributes_kind]
let to_proc_attr = function[@warning "-8"] Sqlite3.Data.BLOB b -> Marshal.from_string b 0
let hash = Hashtbl.hash
end)
let pname_to_key = KeyHashtbl.create 16
let key_of_pname_pkind (pname, pkind as p) =
try KeyHashtbl.find pname_to_key p
with Not_found ->
let key = Typ.Procname.to_filename pname ^ string_of_pkind pkind |> Store.blob_of_key in
KeyHashtbl.replace pname_to_key p key ; key
let load_aux ?(min_kind= ProcUndefined) pname =
List.find_map (most_relevant_down_to_proc_kind_inclusive min_kind) ~f:(fun pkind ->
key_of_pname_pkind (pname, pkind) |> Store.find )
let of_proc_attr x = Sqlite3.Data.BLOB (Marshal.to_string x [])
end
let load pname : ProcAttributes.t option = load_aux pname
let get_replace_statement =
(* The innermost SELECT returns either the current attributes_kind and source_file associated with
the given proc name, or default values of (-1,""). These default values have the property that
they are always "less than" any legit value. More precisely, MAX ensures that some value is
returned even if there is no row satisfying WHERE (we'll get NULL in that case, the value in
the row otherwise). COALESCE then returns the first non-NULL value, which will be either the
value of the row corresponding to that pname in the DB, or the default if no such row exists.
The next (second-outermost) SELECT filters out that value if it is "more defined" than the ones
we would like to insert (which will never be the case if the default values are returned). If
not, it returns a trivial row (consisting solely of NULL since we don't use its values) and the
INSERT OR REPLACE will proceed and insert or update the values stored into the DB for that
pname. *)
(* TRICK: use the source file to be more deterministic in case the same procedure name is defined
in several files *)
(* TRICK: older versions of sqlite (prior to version 3.15.0 (2016-10-14)) do not support row
values so the lexicographic ordering for (:akind, :sfile) is done by hand *)
(* TODO (optim): it might be worth not generating the source file everytime we do a store, but
only generate it if the attribute needs updating (which should be orders of magnitude less
frequent) *)
ResultsDir.register_statement
{|
INSERT OR REPLACE INTO attributes
SELECT :pname, :akind, :sfile, :pattr
FROM (
SELECT NULL
FROM (
SELECT COALESCE(MAX(attr_kind),-1) AS attr_kind,
COALESCE(MAX(source_file),"") AS source_file
FROM attributes
WHERE proc_name = :pname )
WHERE attr_kind < :akind
OR (attr_kind = :akind AND source_file < :sfile) )|}
let replace pname_blob akind loc_file attr_blob =
let replace_stmt = get_replace_statement () in
Sqlite3.bind replace_stmt 1 (* :pname *) pname_blob
|> SqliteUtils.check_sqlite_error ~log:"replace bind pname" ;
Sqlite3.bind replace_stmt 2 (* :akind *) (Sqlite3.Data.INT (int64_of_attributes_kind akind))
|> SqliteUtils.check_sqlite_error ~log:"replace bind attribute kind" ;
Sqlite3.bind replace_stmt 3 (* :sfile *) loc_file
|> SqliteUtils.check_sqlite_error ~log:"replace bind source file" ;
Sqlite3.bind replace_stmt 4 (* :pattr *) attr_blob
|> SqliteUtils.check_sqlite_error ~log:"replace bind proc attributes" ;
SqliteUtils.sqlite_unit_step ~finalize:false ~log:"Attributes.replace" replace_stmt
let get_select_statement =
ResultsDir.register_statement "SELECT proc_attributes FROM attributes WHERE proc_name = :k"
let get_select_defined_statement =
ResultsDir.register_statement
"SELECT proc_attributes FROM attributes WHERE proc_name = :k AND attr_kind = %Ld"
(int64_of_attributes_kind ProcDefined)
let find ~defined pname_blob =
let select_stmt = if defined then get_select_defined_statement () else get_select_statement () in
Sqlite3.bind select_stmt 1 pname_blob
|> SqliteUtils.check_sqlite_error ~log:"find bind proc name" ;
SqliteUtils.sqlite_result_step ~finalize:false ~log:"Attributes.find" select_stmt
|> Option.map ~f:Data.to_proc_attr
let load pname = Data.of_pname pname |> find ~defined:false
let store (attr: ProcAttributes.t) =
let pkind = proc_kind_of_attr attr in
if load attr.proc_name |> Option.value_map ~default:true ~f:(should_override_attr attr) then
(* NOTE: We need to do this dance of adding the proc_kind to the key because there's a race condition between the time we load the attributes from the db and the time we write possibly better ones. We could avoid this by making the db schema richer than just key/value and turning the SELECT + REPLACE into an atomic transaction. *)
let key = key_of_pname_pkind (attr.proc_name, pkind) in
Store.replace key (Store.blob_of_value attr) ;
least_relevant_up_to_proc_kind_exclusive pkind
|> List.iter ~f:(fun k -> key_of_pname_pkind (attr.proc_name, k) |> Store.delete)
let load_defined pname = load_aux ~min_kind:ProcDefined pname
let key = Data.of_pname attr.proc_name in
replace key pkind (Data.of_source_file attr.loc.Location.file) (Data.of_proc_attr attr)
let load_defined pname = Data.of_pname pname |> find ~defined:true
let get_correct_type_from_objc_class_name type_name =
(* ToDo: this function should return a type that includes a reference to the tenv computed by:

@ -11,12 +11,6 @@
open! IStd
type attributes_kind
module Table : KeyValue.Table with type key = string and type value = ProcAttributes.t
module Store : KeyValue.S with module Table = Table
val store : ProcAttributes.t -> unit
(** Save .attr file for the procedure into the attributes database. *)

@ -547,6 +547,8 @@ module Procname = struct
let equal = [%compare.equal : t]
let hash = Hashtbl.hash
(** Level of verbosity of some to_string functions. *)
type detail_level = Verbose | Non_verbose | Simple [@@deriving compare]
@ -949,6 +951,8 @@ module Procname = struct
| Linters_dummy_method
-> to_unique_id p
let sexp_of_t p = Sexp.Atom (to_string p)
(** Convenient representation of a procname for external tools (e.g. eclipse plugin) *)
let to_simplified_string ?(withclass= false) p =
match p with

@ -281,7 +281,10 @@ module Procname : sig
[@@deriving compare]
val equal : t -> t -> bool
(** Equality for proc names. *)
val hash : t -> int
val sexp_of_t : t -> Sexp.t
type java_type = string option * string

@ -1,137 +0,0 @@
(*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! IStd
module L = Logging
module type Table = sig
type key
type value
val table : string
end
module type BlobInternal = sig
module Table : Table
type key_blob = Sqlite3.Data.t
type value_blob = Sqlite3.Data.t
val blob_of_key : Table.key -> key_blob
val blob_of_value : Table.value -> value_blob
val value_of_blob : value_blob -> Table.value option
end
module type S = sig
module Table : Table
type key_blob
type value_blob
val blob_of_key : Table.key -> key_blob
val blob_of_value : Table.value -> value_blob
external key_blob_of_data : Sqlite3.Data.t -> key_blob = "%identity"
external value_blob_of_data : Sqlite3.Data.t -> value_blob = "%identity"
val value_of_blob : value_blob -> Table.value option
val replace : key_blob -> value_blob -> unit
val find : key_blob -> Table.value option
val delete : key_blob -> unit
end
(* The functor is mostly here to provide a modicum of type safety around blobing/unblobing *)
module Make (Table : Table) : S with module Table = Table = struct
module Unsafe : BlobInternal with module Table = Table = struct
module Table = Table
type key_blob = Sqlite3.Data.t
type value_blob = Sqlite3.Data.t
let blob x = Sqlite3.Data.BLOB (Marshal.to_string x [])
let unblob = function
| Sqlite3.Data.BLOB b
-> Some (Marshal.from_string b 0)
| Sqlite3.Data.NULL
-> None
| _
-> assert false
let blob_of_key = blob
let blob_of_value = blob
let value_of_blob = unblob
end
(* cannot mix, e.g., blob_key and blob_value now *)
include Unsafe
external key_blob_of_data : Sqlite3.Data.t -> key_blob = "%identity"
external value_blob_of_data : Sqlite3.Data.t -> value_blob = "%identity"
let register_statement stmt_fmt =
let k stmt0 =
let stmt_ref = ref None in
let new_statement db =
let stmt = Sqlite3.prepare db stmt0 in
ResultsDir.on_close_database ~f:(fun _ ->
Option.iter !stmt_ref ~f:(SqliteUtils.finalize ~log:"db close callback") ) ;
stmt_ref := Some stmt
in
ResultsDir.on_new_database_connection ~f:new_statement ;
fun () ->
match !stmt_ref with
| None
-> L.(die InternalError) "database not initialized"
| Some stmt
-> Sqlite3.reset stmt |> SqliteUtils.check_sqlite_error ~log:"reset prepared statement" ;
Sqlite3.clear_bindings stmt
|> SqliteUtils.check_sqlite_error ~log:"clear bindings of prepared statement" ;
stmt
in
Printf.ksprintf k stmt_fmt
let get_replace_statement =
register_statement "REPLACE INTO %s(key, value) VALUES(:k, :v)" Table.table
let replace key_blob value_blob =
let replace_stmt = get_replace_statement () in
Sqlite3.bind replace_stmt 1 key_blob |> SqliteUtils.check_sqlite_error ~log:"replace bind key" ;
Sqlite3.bind replace_stmt 2 value_blob
|> SqliteUtils.check_sqlite_error ~log:"replace bind value" ;
SqliteUtils.sqlite_unit_step ~finalize:false ~log:"KeyValue.replace" replace_stmt
let get_select_statement = register_statement "SELECT value FROM %s WHERE key = :k" Table.table
let find key_blob =
let select_stmt = get_select_statement () in
Sqlite3.bind select_stmt 1 key_blob |> SqliteUtils.check_sqlite_error ~log:"insert bind key" ;
SqliteUtils.sqlite_result_step ~finalize:false ~log:"KeyValue.find" select_stmt
|> Option.bind ~f:value_of_blob
let get_delete_statement = register_statement "DELETE FROM %s WHERE key = :k" Table.table
let delete key_blob =
let delete_stmt = get_delete_statement () in
Sqlite3.bind delete_stmt 1 key_blob |> SqliteUtils.check_sqlite_error ~log:"delete bind key" ;
SqliteUtils.sqlite_unit_step ~finalize:false ~log:"KeyValue.delete" delete_stmt
end

@ -1,48 +0,0 @@
(*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*)
open! IStd
(** key/value database information *)
module type Table = sig
type key
type value
val table : string
end
(** Key/value store backed by the ResultsDir database *)
module type S = sig
module Table : Table
type key_blob
type value_blob
val blob_of_key : Table.key -> key_blob
(** turn a key into a [Sqlite3.Data.BLOB] *)
val blob_of_value : Table.value -> value_blob
(** turn a value into a [Sqlite3.Data.BLOB] *)
external key_blob_of_data : Sqlite3.Data.t -> key_blob = "%identity"
external value_blob_of_data : Sqlite3.Data.t -> value_blob = "%identity"
val value_of_blob : value_blob -> Table.value option
val replace : key_blob -> value_blob -> unit
val find : key_blob -> Table.value option
val delete : key_blob -> unit
end
module Make (Table : Table) : S with module Table = Table

@ -9,34 +9,46 @@
open! IStd
module L = Logging
let merge_attributes_table ~into ~db_name ~db_file =
(* no need to wrap this in a single transaction (to batch writes) because we open the table with
synchronous=OFF *)
(* do not go through Attributes so as not to deserialize and reserialize objects pointlessly, and
so as not to fill the cache with all the attributes (especially since this function will be
called before forking all the analysis processes. *)
let merge_attributes_table ~db_file =
(* Do the merge purely in SQL for great speed. The query works by doing a left join between the
sub-table and the main one, and applying the same "more defined" logic as in Attributes in the
cases where a proc_name is present in both the sub-table and the main one (main_attr_kind !=
NULL). All the rows that pass this filter are inserted/updated into the main table. *)
let copy_stmt =
Sqlite3.prepare into
(Printf.sprintf "REPLACE INTO %s SELECT * FROM %s.%s" ResultsDir.attributes_table db_name
ResultsDir.attributes_table)
Sqlite3.prepare (ResultsDir.get_database ())
{|
INSERT OR REPLACE INTO attributes
SELECT proc_name, attr_kind, source_file, proc_attributes
FROM (
SELECT sub.proc_name, sub.attr_kind, sub.source_file, sub.proc_attributes,
main.attr_kind AS main_attr_kind, main.source_file AS main_source_file
FROM (
attached.attributes AS sub
LEFT JOIN attributes AS main
ON sub.proc_name = main.proc_name ) )
WHERE
main_attr_kind is NULL
OR main_attr_kind < attr_kind
OR (main_attr_kind = attr_kind AND main_source_file < source_file)
|}
in
SqliteUtils.sqlite_unit_step ~log:(Printf.sprintf "copying contents of database '%s'" db_file)
copy_stmt
let merge ~into db_file =
let db_name = "db" in
let merge ~db_file =
(* no need to wrap all the individual table merges in a single transaction (to batch writes)
because we open the table with synchronous=OFF *)
let main_db = ResultsDir.get_database () in
SqliteUtils.check_sqlite_error ~fatal:true
~log:(Printf.sprintf "attaching database '%s'" db_file)
(Sqlite3.exec into (Printf.sprintf "ATTACH '%s' AS %s" db_file db_name)) ;
let do_merge () = merge_attributes_table ~into ~db_name ~db_file in
Utils.without_gc ~f:do_merge ;
(Sqlite3.exec main_db (Printf.sprintf "ATTACH '%s' AS attached" db_file)) ;
merge_attributes_table ~db_file ;
SqliteUtils.check_sqlite_error ~fatal:true
~log:(Printf.sprintf "detaching database '%s'" db_file)
(Sqlite3.exec into (Printf.sprintf "DETACH %s" db_name)) ;
(Sqlite3.exec main_db "DETACH attached") ;
()
let merge_buck_flavors_results infer_deps_file =
let into = ResultsDir.get_database () in
let one_line line =
match String.split ~on:'\t' line with
| [_; _; target_results_dir]
@ -45,7 +57,7 @@ let merge_buck_flavors_results infer_deps_file =
Filename.dirname (Config.project_root ^/ "buck-out") ^/ target_results_dir
else target_results_dir
in
merge ~into (infer_out_src ^/ ResultsDir.database_filename)
merge ~db_file:(infer_out_src ^/ ResultsDir.database_filename)
| _
-> assert false
in

@ -10,4 +10,5 @@
open! IStd
val merge_buck_flavors_results : string -> unit
(** Merge the results from sub-invocations of infer inside buck-out/. Takes as argument the infer_deps file. *)
(** Merge the results from sub-invocations of infer inside buck-out/. Takes as argument the
infer_deps file. *)

@ -16,8 +16,6 @@ let database_filename = "results.db"
let database_fullpath = Config.results_dir ^/ database_filename
let attributes_table = "attributes"
let results_dir_dir_markers =
List.map ~f:(Filename.concat Config.results_dir) [Config.captured_dir_name; Config.specs_dir_name]
@ -48,10 +46,16 @@ let remove_results_dir () =
Utils.rmtree Config.results_dir )
let create_attributes_table db =
(* it would be nice to use "WITHOUT ROWID" here but ancient versions of sqlite do not support
it *)
SqliteUtils.exec db ~log:"initializing results DB"
~stmt:
(Printf.sprintf "CREATE TABLE IF NOT EXISTS %s (key BLOB PRIMARY KEY ,value BLOB)"
attributes_table)
{|
CREATE TABLE IF NOT EXISTS attributes
( proc_name TEXT PRIMARY KEY
, attr_kind INTEGER NOT NULL
, source_file TEXT NOT NULL
, proc_attributes BLOB NOT NULL )|}
let create_db () =
let temp_db = Filename.temp_file ~in_dir:Config.results_dir database_filename ".tmp" in
@ -112,8 +116,7 @@ let get_database () = Option.value_exn !database
let reset_attributes_table () =
let db = get_database () in
SqliteUtils.exec db ~log:"drop attributes table"
~stmt:(Printf.sprintf "DROP TABLE %s" attributes_table) ;
SqliteUtils.exec db ~log:"drop attributes table" ~stmt:"DROP TABLE attributes" ;
create_attributes_table db
let delete_capture_and_analysis_data () =
@ -126,3 +129,29 @@ let delete_capture_and_analysis_data () =
let canonicalize_db () =
let db = get_database () in
SqliteUtils.exec db ~log:"running VACUUM" ~stmt:"VACUUM"
let register_statement stmt_fmt =
let k stmt0 =
let stmt_ref = ref None in
let new_statement db =
let stmt =
try Sqlite3.prepare db stmt0
with Sqlite3.Error error ->
L.die InternalError "Could not prepare the following statement:@\n%s@\nReason: %s" stmt0
error
in
on_close_database ~f:(fun _ -> SqliteUtils.finalize ~log:"db close callback" stmt) ;
stmt_ref := Some stmt
in
on_new_database_connection ~f:new_statement ;
fun () ->
match !stmt_ref with
| None
-> L.(die InternalError) "database not initialized"
| Some stmt
-> Sqlite3.reset stmt |> SqliteUtils.check_sqlite_error ~log:"reset prepared statement" ;
Sqlite3.clear_bindings stmt
|> SqliteUtils.check_sqlite_error ~log:"clear bindings of prepared statement" ;
stmt
in
Printf.ksprintf k stmt_fmt

@ -15,9 +15,6 @@ val database_filename : string
val get_database : unit -> Sqlite3.db
(** The results database. You should always use this function to access the database, as the connection to it may change during the execution (see [new_database_connection]). *)
val attributes_table : string
(** the name of the table of proc names to their proc attributes *)
val reset_attributes_table : unit -> unit
(** zero out the attributes table *)
@ -39,6 +36,11 @@ val delete_capture_and_analysis_data : unit -> unit
val canonicalize_db : unit -> unit
(** put the database on disk in deterministic form *)
val on_new_database_connection : f:(Sqlite3.db -> unit) -> unit
val register_statement : ('a, unit, string, (unit -> Sqlite3.stmt)) Base.format4 -> 'a
(** Return a function unit -> Sqlite3.stmt that can be called (once the DB has been initialized) to
get the prepared statement corresponding to the current DB connection. Use this to prepare
statements only once per DB connection.
val on_close_database : f:(Sqlite3.db -> unit) -> unit
In particular, clients of this need not worry about calling [Sqlite3.finalize] on the returned
statement, or about generating new statements when the connection to the DB changes: this is all
handled internally. *)

@ -54,6 +54,7 @@ let common_libraries =
(if java then ["javalib"; "ptrees"; "sawja"] else [])
@ [ "ANSITerminal"
; "atdgen"
; "base"
; "cmdliner"
; "core"
; "extlib"

@ -10,7 +10,7 @@ ROOT_DIR = $(TESTS_DIR)/../..
ANALYZER = infer
BUCK_TARGET = //src:hello @buck_target.txt
SOURCES = $(wildcard src/hello.c)
SOURCES = $(wildcard src/*.c) $(wildcard src/subtarget1/*.c) $(wildcard src/subtarget2/*.c)
OBJECTS = buck-out/gen/src/hello\#compile-hello.c.o1f717d69,default/hello.c.o
INFER_OPTIONS = --report-custom-error --developer-mode --project-root $(TESTS_DIR)
INFERPRINT_OPTIONS = --project-root $(TESTS_DIR) --issues-tests

@ -1,3 +1,5 @@
src/hello.c, test, 2, NULL_DEREFERENCE, [start of procedure test()]
src/hello2.c, test2, 2, NULL_DEREFERENCE, [start of procedure test2()]
src/hello3.c, test3, 2, NULL_DEREFERENCE, [start of procedure test3()]
src/subtarget1/z_filename_greater_than_subhello1.c, foo_defined_in_subtarget1, 2, NULL_DEREFERENCE, [start of procedure foo_defined_in_subtarget1()]
src/subtarget2/subhello2.c, goo, 3, NULL_DEREFERENCE, [start of procedure goo(),Skipping foo_defined_in_subtarget1(): function or method not found]

@ -3,6 +3,10 @@ cxx_library(
srcs = [
'hello.c', 'hello2.c',
],
deps = [
"//src/subtarget1:subtarget1",
"//src/subtarget2:subtarget2",
]
)
cxx_library(

@ -0,0 +1,10 @@
cxx_library(
name = 'subtarget1',
visibility = [
'PUBLIC',
],
srcs = [
'z_filename_greater_than_subhello1.c',
'subhello1.c',
],
)

@ -0,0 +1,15 @@
/*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#include <stdlib.h>
void foo_defined_in_subtarget1() {
int* s = NULL;
*s = 42;
}

@ -0,0 +1,15 @@
/*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#include <stdlib.h>
void foo_defined_in_subtarget1() {
int* t = NULL;
*t = 42;
}

@ -0,0 +1,9 @@
cxx_library(
name = 'subtarget2',
visibility = [
'PUBLIC',
],
srcs = [
'subhello2.c',
],
)

@ -0,0 +1,18 @@
/*
* Copyright (c) 2017 - present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#include <stdlib.h>
void foo_defined_in_subtarget1();
void goo() {
foo_defined_in_subtarget1();
int* s = NULL;
*s = 42;
}
Loading…
Cancel
Save