From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by sourceware.org (Postfix) with ESMTPS id 2922A3858409 for ; Tue, 21 Nov 2023 22:20:25 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 2922A3858409 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 2922A3858409 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1700605229; cv=none; b=xR5rYb8xX5Ldxwa15X9iahr+kDNCHqJSQPFRJcf7nxyC/XqwXbyZ5mZ60Z678IZh4YWjk832RyCZu934val694JVw3tUONjVd9TTKdz/QqbWTbGWGmcixsTEaUfAfRPtIeLxYqaxAiw5Uk0OAHqovBbpMS2pfbXYWazXRpztRZ8= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1700605229; c=relaxed/simple; bh=QsItktenCrAl0lsSuWPgc4DBpTlB06TwS9phKCufLA0=; h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version; b=DIiR12PeLoFPsqXy5U1Hmrv7eBCTtRbuOY5NspeRrzKyljBIieU8CVDtPCF6EyCNBfhopIELay468VPc0BugJ8n/4W6t80ERMaSPmwvHI8e94VSRqfOfGIltNpm9w/oLhjY3XV03b9I2VU6uS8LkyQYbYUdCPjG3xM3fogsAjW4= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1700605224; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=S3R/UKSHqVf6cxkB9as28qAuOn5QmSMFbqi+Y1nbba0=; b=GIgs7zlXBGzYH/THPbVCpNAY0r0vIeOjKuxs+MEghm11xQEbOogP6mVZM5JKy/w7qp2MQ9 945KfqyeNfUFi03FkOYmN6mmN0+YujRgETgQExNr0KVxREYFundGVFTbRBKgHG42XbAukp WFAxDg7lHPWBVBRWZ+BS7YkoJ8zq0a8= Received: from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-526-9r5GtCBCOA6I4qIFADr8YA-1; Tue, 21 Nov 2023 17:20:22 -0500 X-MC-Unique: 9r5GtCBCOA6I4qIFADr8YA-1 Received: from smtp.corp.redhat.com (int-mx10.intmail.prod.int.rdu2.redhat.com [10.11.54.10]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 79CFF828B20; Tue, 21 Nov 2023 22:20:22 +0000 (UTC) Received: from t14s.localdomain.com (unknown [10.22.10.115]) by smtp.corp.redhat.com (Postfix) with ESMTP id 337A1492BFA; Tue, 21 Nov 2023 22:20:22 +0000 (UTC) From: David Malcolm To: gcc-patches@gcc.gnu.org, binutils@sourceware.org Cc: Nick Clifton , Simon Sobisch , David Malcolm Subject: [PATCH 3/5] libdiagnostics v2: add C++ wrapper API Date: Tue, 21 Nov 2023 17:20:16 -0500 Message-Id: <20231121222019.646253-4-dmalcolm@redhat.com> In-Reply-To: <20231121222019.646253-1-dmalcolm@redhat.com> References: <20231106222959.2707741-1-dmalcolm@redhat.com> <20231121222019.646253-1-dmalcolm@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.11.54.10 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset="US-ASCII"; x-default=true X-Spam-Status: No, score=-10.4 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH,DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,GIT_PATCH_0,KAM_SHORT,LIKELY_SPAM_BODY,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL,SPF_HELO_NONE,SPF_NONE,TXREP,T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org List-Id: This is new in v2: a C++ wrapper API that provides some syntactic sugar for calling into libdiagnostics.{h,so}. I've been "eating my own dogfood" with this by using it to write a simple client that reads a SARIF file and dumps it using the text sink: https://github.com/davidmalcolm/libdiagnostics-sarif-dump gcc/ChangeLog: * libdiagnostics++.h: New file. gcc/testsuite/ChangeLog: * libdiagnostics.dg/libdiagnostics.exp: Add .cc tests. * libdiagnostics.dg/test-error-with-note.cc: New test. * libdiagnostics.dg/test-error.cc: New test. * libdiagnostics.dg/test-fix-it-hint.cc: New test. * libdiagnostics.dg/test-helpers++.h: New header. * libdiagnostics.dg/test-labelled-ranges.cc: New test. --- gcc/libdiagnostics++.h | 378 ++++++++++++++++++ .../libdiagnostics.dg/libdiagnostics.exp | 8 +- .../libdiagnostics.dg/test-error-with-note.cc | 47 +++ gcc/testsuite/libdiagnostics.dg/test-error.cc | 40 ++ .../libdiagnostics.dg/test-fix-it-hint.cc | 44 ++ .../libdiagnostics.dg/test-helpers++.h | 28 ++ .../libdiagnostics.dg/test-labelled-ranges.cc | 43 ++ 7 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 gcc/libdiagnostics++.h create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error.cc create mode 100644 gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc create mode 100644 gcc/testsuite/libdiagnostics.dg/test-helpers++.h create mode 100644 gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc diff --git a/gcc/libdiagnostics++.h b/gcc/libdiagnostics++.h new file mode 100644 index 000000000000..8f412b07aa78 --- /dev/null +++ b/gcc/libdiagnostics++.h @@ -0,0 +1,378 @@ +/* A C++ wrapper API around libdiagnostics.h for emitting diagnostics. + Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3, or (at your option) +any later version. + +GCC is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +. */ + +#ifndef LIBDIAGNOSTICSPP_H +#define LIBDIAGNOSTICSPP_H + +#include "libdiagnostics.h" + +namespace libdiagnostics { + +typedef diagnostic_line_num_t line_num_t; +typedef diagnostic_column_num_t column_num_t; + +class file; +class physical_location; +class logical_location; +class group; +class manager; +class diagnostic; + +/* Wrapper around a const diagnostic_file *. */ + +class file +{ +public: + file (const diagnostic_file *file) : m_inner (file) {} + + const diagnostic_file * const m_inner; +}; + +/* Wrapper around a const diagnostic_physical_location *. */ + +class physical_location +{ +public: + physical_location (const diagnostic_physical_location *location) + : m_inner (location) + {} + + const diagnostic_physical_location *m_inner; +}; + +/* Wrapper around a const diagnostic_logical_location *. */ + +class logical_location +{ +public: + logical_location () : m_inner (nullptr) {} + + logical_location (const diagnostic_logical_location *logical_loc) + : m_inner (logical_loc) + {} + + const diagnostic_logical_location * const m_inner; +}; + +/* RAII class for starting/ending a group within a diagnostic_manager. */ + +class group +{ +public: + group (manager &mgr); + ~group (); + +private: + manager &m_mgr; +}; + +/* Wrapper around a diagnostic *. */ + +class diagnostic +{ +public: + diagnostic (::diagnostic *d) : m_inner (d) {} + + void + set_cwe (unsigned cwe_id); + + void + set_location (physical_location loc); + + void + add_location_with_label (physical_location loc, + const char *text); + + void + set_logical_location (logical_location loc); + + void + add_fix_it_hint_insert_before (physical_location loc, + const char *addition); + void + add_fix_it_hint_insert_after (physical_location loc, + const char *addition); + void + add_fix_it_hint_replace (physical_location loc, + const char *replacement); + void + add_fix_it_hint_delete (physical_location loc); + + void + finish (const char *fmt, ...) + LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2) + LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 3); + + ::diagnostic * const m_inner; +}; + +/* Wrapper around ownership of a diagnostic_manager *. */ + +class manager +{ +public: + manager () + : m_inner (diagnostic_manager_new ()) + { + } + ~manager () + { + diagnostic_manager_release (m_inner); + } + + void + add_text_sink (FILE *dst_stream, + enum diagnostic_colorize colorize) + { + diagnostic_manager_add_text_sink (m_inner, dst_stream, colorize); + } + + void + add_sarif_sink (FILE *dst_stream, + enum diagnostic_sarif_version version) + { + diagnostic_manager_add_sarif_sink (m_inner, dst_stream, version); + } + + void + write_patch (FILE *dst_stream) + { + diagnostic_manager_write_patch (m_inner, dst_stream); + } + + /* Location management. */ + + file + new_file (const char *name, + const char *sarif_source_language) + LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2) + LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3); + + void + debug_dump (file f, + FILE *out); + + physical_location + new_location_from_file_and_line (file f, diagnostic_line_num_t line_num); + + physical_location + new_location_from_file_line_column (file f, + line_num_t line_num, + column_num_t column_num); + + physical_location + new_location_from_range (physical_location loc_caret, + physical_location loc_start, + physical_location loc_end); + + void + debug_dump (physical_location loc, + FILE *out); + + logical_location + new_logical_location (enum diagnostic_logical_location_kind_t kind, + logical_location parent, + const char *short_name, + const char *fully_qualified_name, + const char *decorated_name); + + void + debug_dump (logical_location loc, + FILE *out); + + diagnostic + begin_diagnostic (enum diagnostic_level level); + + + diagnostic_manager * const m_inner; +}; + +// Implementation + +// class group + +inline +group::group (manager &mgr) +: m_mgr (mgr) +{ + diagnostic_manager_begin_group (m_mgr.m_inner); +} + +inline +group::~group () +{ + diagnostic_manager_end_group (m_mgr.m_inner); +} + +// class diagnostic + +inline void +diagnostic::set_cwe (unsigned cwe_id) +{ + diagnostic_set_cwe (m_inner, cwe_id); +} + +inline void +diagnostic::set_location (physical_location loc) +{ + diagnostic_set_location (m_inner, loc.m_inner); +} + +inline void +diagnostic::add_location_with_label (physical_location loc, + const char *text) +{ + diagnostic_add_location_with_label (m_inner, loc.m_inner, text); +} + +inline void +diagnostic::set_logical_location (logical_location loc) +{ + diagnostic_set_logical_location (m_inner, loc.m_inner); +} + +inline void +diagnostic::add_fix_it_hint_insert_before (physical_location loc, + const char *addition) +{ + diagnostic_add_fix_it_hint_insert_before (m_inner, + loc.m_inner, + addition); +} + +inline void +diagnostic::add_fix_it_hint_insert_after (physical_location loc, + const char *addition) +{ + diagnostic_add_fix_it_hint_insert_after (m_inner, + loc.m_inner, + addition); +} + +inline void +diagnostic::add_fix_it_hint_replace (physical_location loc, + const char *replacement) +{ + diagnostic_add_fix_it_hint_replace (m_inner, + loc.m_inner, + replacement); +} + +inline void +diagnostic::add_fix_it_hint_delete (physical_location loc) +{ + diagnostic_add_fix_it_hint_delete (m_inner, + loc.m_inner); +} + +inline void +diagnostic::finish (const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + diagnostic_finish_va (m_inner, fmt, &ap); + va_end (ap); +} + +// class manager + +inline file +manager::new_file (const char *name, + const char *sarif_source_language) +{ + return file + (diagnostic_manager_new_file (m_inner, name, sarif_source_language)); +} + +inline physical_location +manager::new_location_from_file_and_line (file f, + diagnostic_line_num_t line_num) +{ + return physical_location + (diagnostic_manager_new_location_from_file_and_line (m_inner, + f.m_inner, + line_num)); +} + +inline physical_location +manager::new_location_from_file_line_column (file f, + line_num_t line_num, + column_num_t column_num) +{ + return physical_location + (diagnostic_manager_new_location_from_file_line_column (m_inner, + f.m_inner, + line_num, + column_num)); +} + +inline physical_location +manager::new_location_from_range (physical_location loc_caret, + physical_location loc_start, + physical_location loc_end) +{ + return physical_location + (diagnostic_manager_new_location_from_range (m_inner, + loc_caret.m_inner, + loc_start.m_inner, + loc_end.m_inner)); +} + +inline void +manager::debug_dump (physical_location loc, + FILE *out) +{ + diagnostic_manager_debug_dump_location (m_inner, + loc.m_inner, + out); +} +inline logical_location +manager::new_logical_location (enum diagnostic_logical_location_kind_t kind, + logical_location parent, + const char *short_name, + const char *fully_qualified_name, + const char *decorated_name) +{ + return logical_location + (diagnostic_manager_new_logical_location (m_inner, + kind, + parent.m_inner, + short_name, + fully_qualified_name, + decorated_name)); +} + +inline void +manager::debug_dump (logical_location loc, + FILE *out) +{ + diagnostic_manager_debug_dump_logical_location (m_inner, + loc.m_inner, + out); +} + +inline diagnostic +manager::begin_diagnostic (enum diagnostic_level level) +{ + return diagnostic (diagnostic_begin (m_inner, level)); +} + +} // namespace libdiagnostics + +#endif // #ifndef LIBDIAGNOSTICSPP_H diff --git a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp index bd50a5568a73..bcceb669d3ff 100644 --- a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp +++ b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp @@ -345,10 +345,10 @@ dg-init # Gather a list of all tests. -# C tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.c -set tests [find $srcdir/$subdir test-*.c] - -set tests [lsort $tests] +# C and C++ tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.{c,c++} +set c_tests [find $srcdir/$subdir test-*.c] +set cxx_tests [find $srcdir/$subdir test-*.cc] +set tests [concat $c_tests $cxx_tests] verbose "tests: $tests" diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc new file mode 100644 index 000000000000..e20b9e04d6b1 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc @@ -0,0 +1,47 @@ +/* C++ example of emitting an error with an associated note. + + Intended output is similar to: + +PATH/test-error-with-note.c:6: error: can't find 'foo' + 6 | PRINT "hello world!"; + | ^~~~~~~~~~~~ +PATH/test-error-with-note.c:6: note: have you looked behind the couch? + + along with the equivalent in SARIF. */ + +#include "libdiagnostics++.h" + +/* +_________111111111122 +123456789012345678901 +PRINT "hello world!"; +*/ +const int line_num = __LINE__ - 2; + +int +main () +{ + libdiagnostics::manager mgr; + + mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY); + mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0); + + auto file = mgr.new_file (__FILE__, "c"); + auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8); + auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19); + auto loc_range = mgr.new_location_from_range (loc_start, + loc_start, + loc_end); + + libdiagnostics::group g (mgr); + + auto err (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR)); + err.set_location (loc_range); + err.finish ("can't find %qs", "foo"); + + auto note = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_NOTE); + note.set_location (loc_range); + note.finish ("have you looked behind the couch?"); + + return 0; +}; diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.cc b/gcc/testsuite/libdiagnostics.dg/test-error.cc new file mode 100644 index 000000000000..019307815bfb --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-error.cc @@ -0,0 +1,40 @@ +/* C++ example of emitting an error. + + Intended output is similar to: + +PATH/test-error.cc:6: error: can't find 'foo' + 6 | PRINT "hello world!"; + | ^~~~~~~~~~~~ + + along with the equivalent in SARIF. */ + +#include "libdiagnostics++.h" + +/* +_________111111111122 +123456789012345678901 +PRINT "hello world!"; +*/ +const int line_num = __LINE__ - 2; + +int +main () +{ + libdiagnostics::manager mgr; + + mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY); + mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0); + + auto file = mgr.new_file (__FILE__, "c"); + auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8); + auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19); + auto loc_range = mgr.new_location_from_range (loc_start, + loc_start, + loc_end); + + libdiagnostics::diagnostic d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR)); + d.set_location (loc_range); + d.finish ("can't find %qs", "foo"); + + return 0; +}; diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc new file mode 100644 index 000000000000..6af2641f2804 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc @@ -0,0 +1,44 @@ +/* C++ example of a fix-it hint, including patch generation. + + Intended output is similar to: + +PATH/test-fix-it-hint.cc:19: error: unknown field 'colour'; did you mean 'color' + 19 | return p->colour; + | ^~~~~~ + | color + + along with the equivalent in SARIF, and a generated patch (on stderr) to + make the change. */ + +#include "libdiagnostics++.h" +#include "test-helpers++.h" + +/* +_________11111111112 +12345678901234567890 + return p->colour; +*/ +const int line_num = __LINE__ - 2; + +int +main () +{ + libdiagnostics::manager mgr; + + mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY); + mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0); + + auto file = mgr.new_file (__FILE__, "c"); + auto loc_token = make_range (mgr, file, line_num, 13, 18); + + auto d = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR); + d.set_location (loc_token); + + d.add_fix_it_hint_replace (loc_token, "color"); + + d.finish ("unknown field %qs; did you mean %qs", "colour", "color"); + + mgr.write_patch (stderr); + + return 0; +} diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers++.h b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h new file mode 100644 index 000000000000..c8ff2def1ffa --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h @@ -0,0 +1,28 @@ +/* Common utility code shared between test cases. */ + +#ifndef TEST_HELPERSPP_H +#define TEST_HELPERSPP_H + +namespace libdiagnostics { + +inline physical_location +make_range (manager &mgr, + file f, + line_num_t line_num, + column_num_t start_column, + column_num_t end_column) +{ + auto loc_start = mgr.new_location_from_file_line_column (f, + line_num, + start_column); + auto loc_end = mgr.new_location_from_file_line_column (f, + line_num, + end_column); + return mgr.new_location_from_range (loc_start, + loc_start, + loc_end); +} + +} // namespace libdiagnostics + +#endif /* #ifndef TEST_HELPERSPP_H */ diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc new file mode 100644 index 000000000000..35ccf74e1529 --- /dev/null +++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc @@ -0,0 +1,43 @@ +/* C++ example of multiple locations, with labelling of ranges. + + Intended output is similar to: + +PATH/test-labelled-ranges.cc:9: error: mismatching types: 'int' and 'const char *' + 19 | 42 + "foo" + | ~~ ^ ~~~~~ + | | | + | int const char * + + along with the equivalent in SARIF. */ + +#include "libdiagnostics++.h" +#include "test-helpers++.h" + +/* +_________11111111112 +12345678901234567890 + 42 + "foo" +*/ +const int line_num = __LINE__ - 2; + +int +main () +{ + libdiagnostics::manager mgr; + + mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY); + mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0); + + libdiagnostics::file file = mgr.new_file (__FILE__, "c"); + auto loc_operator = mgr.new_location_from_file_line_column (file, line_num, 6); + + auto d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR)); + d.set_location (loc_operator); + d.add_location_with_label (make_range (mgr, file, line_num, 3, 4), + "int"); + d.add_location_with_label (make_range (mgr, file, line_num, 8, 12), + "const char *"); + d.finish ("mismatching types: %qs and %qs", "int", "const char *"); + + return 0; +} -- 2.26.3