add the ability to parse the gccxml preprocessor output in order to reduce the number of false positives; fix wrong wxASSERT in wxMethod::IsOk; provide more help when called with --help
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@52861 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
@@ -11,8 +11,9 @@
|
|||||||
############
|
############
|
||||||
|
|
||||||
|
|
||||||
gccxmloutput="wxapi.xml" # where do we put the gccXML output:
|
gccxmloutput="wxapi.xml" # the file where we store the gccXML output
|
||||||
allheaders="/tmp/wx-all.h" # headers which includes all wx public headers
|
preprocoutput="wxapi-preproc.txt" # the file where we store the preprocessor's #define values
|
||||||
|
allheaders="/tmp/wx-all.h" # the header which includes all wx public headers (autogenerated)
|
||||||
|
|
||||||
# the list of all wxWidgets public headers
|
# the list of all wxWidgets public headers
|
||||||
listcmd="ls wx/*.h wx/aui/*.h wx/html/*.h wx/protocol/*.h wx/richtext/*.h wx/stc/*.h wx/xml/*.h wx/xrc/*.h"
|
listcmd="ls wx/*.h wx/aui/*.h wx/html/*.h wx/protocol/*.h wx/richtext/*.h wx/stc/*.h wx/xml/*.h wx/xrc/*.h"
|
||||||
@@ -52,14 +53,22 @@ done
|
|||||||
flags="@CXXFLAGS@"
|
flags="@CXXFLAGS@"
|
||||||
|
|
||||||
# NOTE: it's important to define __WXDEBUG__ because some functions of wx
|
# NOTE: it's important to define __WXDEBUG__ because some functions of wx
|
||||||
# are declared (and thus parsed by gcc) only if that symbol is defined;
|
# are declared (and thus parsed by gcc) only if that symbol is defined.
|
||||||
# so we remove __WXDEBUG__ symbol from $flags, in case it's defined:
|
# So we remove it from $flags (in case it's defined) and then readd it.
|
||||||
flags=`echo "$flags" | sed -e 's/-pthread//g' | sed -e 's/__WXDEBUG__//g'`
|
flags=`echo "$flags" | sed -e 's/-pthread//g' | sed -e 's/__WXDEBUG__//g'`
|
||||||
|
|
||||||
|
# append some other flags:
|
||||||
|
flags="-I . -I @top_srcdir@/include $flags -D__WXDEBUG__ -D__WX@TOOLKIT@__ -DWXBUILDING $allheaders"
|
||||||
|
|
||||||
# run gccxml with the same flag used for the real compilation of wx sources:
|
# run gccxml with the same flag used for the real compilation of wx sources:
|
||||||
echo "Running gccxml on the $allheaders file..."
|
echo "Running gccxml on the $allheaders file..."
|
||||||
if [[ -f "$gccxmloutput" ]]; then rm $gccxmloutput; fi
|
if [[ -f "$gccxmloutput" ]]; then rm $gccxmloutput; fi
|
||||||
gccxml -I . -I @top_srcdir@/include $flags -D__WX@TOOLKIT@__ -DWXBUILDING $allheaders -fxml=$gccxmloutput
|
gccxml $flags -fxml=$gccxmloutput
|
||||||
|
|
||||||
|
# now get the list of the #defined values for wx headers, so that the result
|
||||||
|
# can be passed to ifacecheck to aid the comparison
|
||||||
|
echo "Running gccxml's preprocessor on the $allheaders file..."
|
||||||
|
gccxml -E -dM $flags >$preprocoutput
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
rm $allheaders
|
rm $allheaders
|
||||||
|
@@ -38,7 +38,9 @@ bool g_verbose = false;
|
|||||||
#define API_DUMP_FILE "dump.api.txt"
|
#define API_DUMP_FILE "dump.api.txt"
|
||||||
#define INTERFACE_DUMP_FILE "dump.interface.txt"
|
#define INTERFACE_DUMP_FILE "dump.interface.txt"
|
||||||
|
|
||||||
#define PROCESS_ONLY_SWITCH "p"
|
#define PROCESS_ONLY_OPTION "p"
|
||||||
|
#define USE_PREPROCESSOR_OPTION "u"
|
||||||
|
|
||||||
#define MODIFY_SWITCH "m"
|
#define MODIFY_SWITCH "m"
|
||||||
#define DUMP_SWITCH "d"
|
#define DUMP_SWITCH "d"
|
||||||
#define HELP_SWITCH "h"
|
#define HELP_SWITCH "h"
|
||||||
@@ -46,7 +48,10 @@ bool g_verbose = false;
|
|||||||
|
|
||||||
static const wxCmdLineEntryDesc g_cmdLineDesc[] =
|
static const wxCmdLineEntryDesc g_cmdLineDesc[] =
|
||||||
{
|
{
|
||||||
{ wxCMD_LINE_OPTION, PROCESS_ONLY_SWITCH, "process-only",
|
{ wxCMD_LINE_OPTION, USE_PREPROCESSOR_OPTION, "use-preproc",
|
||||||
|
"uses the preprocessor output to increase the checker accuracy",
|
||||||
|
wxCMD_LINE_VAL_STRING, wxCMD_LINE_NEEDS_SEPARATOR },
|
||||||
|
{ wxCMD_LINE_OPTION, PROCESS_ONLY_OPTION, "process-only",
|
||||||
"processes only header files matching the given wildcard",
|
"processes only header files matching the given wildcard",
|
||||||
wxCMD_LINE_VAL_STRING, wxCMD_LINE_NEEDS_SEPARATOR },
|
wxCMD_LINE_VAL_STRING, wxCMD_LINE_NEEDS_SEPARATOR },
|
||||||
{ wxCMD_LINE_SWITCH, MODIFY_SWITCH, "modify",
|
{ wxCMD_LINE_SWITCH, MODIFY_SWITCH, "modify",
|
||||||
@@ -71,6 +76,8 @@ public:
|
|||||||
virtual bool OnInit() { m_modify=false; return true; }
|
virtual bool OnInit() { m_modify=false; return true; }
|
||||||
virtual int OnRun();
|
virtual int OnRun();
|
||||||
|
|
||||||
|
bool ParsePreprocessorOutput(const wxString& filename);
|
||||||
|
|
||||||
bool Compare();
|
bool Compare();
|
||||||
int CompareClasses(const wxClass* iface, const wxClassPtrArray& api);
|
int CompareClasses(const wxClass* iface, const wxClassPtrArray& api);
|
||||||
void FixMethod(const wxString& header, const wxMethod* iface, const wxMethod* api);
|
void FixMethod(const wxString& header, const wxMethod* iface, const wxMethod* api);
|
||||||
@@ -92,7 +99,7 @@ protected:
|
|||||||
// was the MODIFY_SWITCH passed?
|
// was the MODIFY_SWITCH passed?
|
||||||
bool m_modify;
|
bool m_modify;
|
||||||
|
|
||||||
// if non-empty, then PROCESS_ONLY_SWITCH was passed and this is the
|
// if non-empty, then PROCESS_ONLY_OPTION was passed and this is the
|
||||||
// wildcard expression to match
|
// wildcard expression to match
|
||||||
wxString m_strToMatch;
|
wxString m_strToMatch;
|
||||||
};
|
};
|
||||||
@@ -103,19 +110,33 @@ int IfaceCheckApp::OnRun()
|
|||||||
{
|
{
|
||||||
long startTime = wxGetLocalTime(); // for timing purpose
|
long startTime = wxGetLocalTime(); // for timing purpose
|
||||||
|
|
||||||
// parse the command line...
|
|
||||||
wxCmdLineParser parser(g_cmdLineDesc, argc, argv);
|
wxCmdLineParser parser(g_cmdLineDesc, argc, argv);
|
||||||
|
parser.SetLogo(
|
||||||
|
wxString::Format("wxWidgets Interface checker utility (built %s against %s)",
|
||||||
|
__DATE__, wxVERSION_STRING));
|
||||||
|
|
||||||
|
// parse the command line...
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
|
wxString preprocFile;
|
||||||
switch (parser.Parse())
|
switch (parser.Parse())
|
||||||
{
|
{
|
||||||
case -1:
|
|
||||||
// HELP_SWITCH was passed
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
case 0:
|
case 0:
|
||||||
if (parser.Found(VERBOSE_SWITCH))
|
if (parser.Found(VERBOSE_SWITCH))
|
||||||
g_verbose = true;
|
g_verbose = true;
|
||||||
|
|
||||||
|
// IMPORTANT: parsing #define values must be done _before_ actually
|
||||||
|
// parsing the GCC/doxygen XML files
|
||||||
|
if (parser.Found(USE_PREPROCESSOR_OPTION, &preprocFile))
|
||||||
|
{
|
||||||
|
if (!ParsePreprocessorOutput(preprocFile))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in any case set basic std preprocessor #defines:
|
||||||
|
m_interface.AddPreprocessorValue("NULL", "0");
|
||||||
|
|
||||||
|
// parse the two XML files which contain the real and the doxygen interfaces
|
||||||
|
// for wxWidgets API:
|
||||||
if (!m_api.Parse(parser.GetParam(0)) ||
|
if (!m_api.Parse(parser.GetParam(0)) ||
|
||||||
!m_interface.Parse(parser.GetParam(1)))
|
!m_interface.Parse(parser.GetParam(1)))
|
||||||
return 1;
|
return 1;
|
||||||
@@ -133,7 +154,7 @@ int IfaceCheckApp::OnRun()
|
|||||||
if (parser.Found(MODIFY_SWITCH))
|
if (parser.Found(MODIFY_SWITCH))
|
||||||
m_modify = true;
|
m_modify = true;
|
||||||
|
|
||||||
if (parser.Found(PROCESS_ONLY_SWITCH, &m_strToMatch))
|
if (parser.Found(PROCESS_ONLY_OPTION, &m_strToMatch))
|
||||||
{
|
{
|
||||||
size_t len = m_strToMatch.Len();
|
size_t len = m_strToMatch.Len();
|
||||||
if (m_strToMatch.StartsWith("\"") &&
|
if (m_strToMatch.StartsWith("\"") &&
|
||||||
@@ -147,6 +168,20 @@ int IfaceCheckApp::OnRun()
|
|||||||
|
|
||||||
PrintStatistics(wxGetLocalTime() - startTime);
|
PrintStatistics(wxGetLocalTime() - startTime);
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
|
|
||||||
|
default:
|
||||||
|
wxPrintf("\nThis utility checks that the interface XML files created by Doxygen are in\n");
|
||||||
|
wxPrintf("synch with the real headers (whose contents are extracted by the gcc XML file).\n\n");
|
||||||
|
wxPrintf("The 'gccXML' parameter should be the wxapi.xml file created by the 'rungccxml.sh'\n");
|
||||||
|
wxPrintf("script which resides in 'utils/ifacecheck'.\n");
|
||||||
|
wxPrintf("The 'doxygenXML' parameter should be the index.xml file created by Doxygen\n");
|
||||||
|
wxPrintf("for the wxWidgets 'interface' folder.\n\n");
|
||||||
|
wxPrintf("Since the gcc XML file does not contain info about #defines, if you use\n");
|
||||||
|
wxPrintf("the -%s option, you'll get a smaller number of false warnings.\n",
|
||||||
|
USE_PREPROCESSOR_OPTION);
|
||||||
|
|
||||||
|
// HELP_SWITCH was passed or a syntax error occurred
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
@@ -462,6 +497,48 @@ void IfaceCheckApp::FixMethod(const wxString& header, const wxMethod* iface, con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IfaceCheckApp::ParsePreprocessorOutput(const wxString& filename)
|
||||||
|
{
|
||||||
|
wxTextFile tf;
|
||||||
|
if (!tf.Open(filename)) {
|
||||||
|
LogError("can't open the '%s' preprocessor output file.", filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t useful = 0;
|
||||||
|
for (unsigned int i=0; i < tf.GetLineCount(); i++)
|
||||||
|
{
|
||||||
|
const wxString& line = tf.GetLine(i);
|
||||||
|
wxString defnameval = line.Mid(8); // what follows the "#define " string
|
||||||
|
|
||||||
|
// the format of this line should be:
|
||||||
|
// #define DEFNAME DEFVALUE
|
||||||
|
if (!line.StartsWith("#define ") || !defnameval.Contains(" ")) {
|
||||||
|
LogError("unexpected content in '%s' at line %d.", filename, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get DEFNAME
|
||||||
|
wxString defname = defnameval.BeforeFirst(' ');
|
||||||
|
if (defname.Contains("("))
|
||||||
|
continue; // this is a macro, skip it!
|
||||||
|
|
||||||
|
// get DEFVAL
|
||||||
|
wxString defval = defnameval.AfterFirst(' ').Strip(wxString::both);
|
||||||
|
if (defval.StartsWith("(") && defval.EndsWith(")"))
|
||||||
|
defval = defval.Mid(1, defval.Len()-2);
|
||||||
|
|
||||||
|
// store this pair in the doxygen interface, where it can be useful
|
||||||
|
m_interface.AddPreprocessorValue(defname, defval);
|
||||||
|
useful++;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogMessage("Parsed %d preprocessor #defines from '%s' which will be used later...",
|
||||||
|
useful, filename);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void IfaceCheckApp::PrintStatistics(long secs)
|
void IfaceCheckApp::PrintStatistics(long secs)
|
||||||
{
|
{
|
||||||
LogMessage("wx real headers contains declaration of %d classes (%d methods)",
|
LogMessage("wx real headers contains declaration of %d classes (%d methods)",
|
||||||
|
@@ -115,10 +115,11 @@ bool wxType::operator==(const wxType& m) const
|
|||||||
// wxArgumentType
|
// wxArgumentType
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
void wxArgumentType::SetDefaultValue(const wxString& defval)
|
void wxArgumentType::SetDefaultValue(const wxString& defval, const wxString& defvalForCmp)
|
||||||
{
|
{
|
||||||
m_strDefaultValue=defval.Strip(wxString::both);
|
m_strDefaultValue=defval.Strip(wxString::both);
|
||||||
|
m_strDefaultValueForCmp=defvalForCmp.Strip(wxString::both);
|
||||||
|
/*
|
||||||
// in order to make valid&simple comparison on argument defaults,
|
// in order to make valid&simple comparison on argument defaults,
|
||||||
// we reduce some of the multiple forms in which the same things may appear
|
// we reduce some of the multiple forms in which the same things may appear
|
||||||
// to a single form:
|
// to a single form:
|
||||||
@@ -128,7 +129,7 @@ void wxArgumentType::SetDefaultValue(const wxString& defval)
|
|||||||
m_strDefaultValue.Replace("0", "NULL");
|
m_strDefaultValue.Replace("0", "NULL");
|
||||||
else
|
else
|
||||||
m_strDefaultValue.Replace("NULL", "0");
|
m_strDefaultValue.Replace("NULL", "0");
|
||||||
|
*/
|
||||||
|
|
||||||
if (m_strDefaultValue.Contains("wxGetTranslation"))
|
if (m_strDefaultValue.Contains("wxGetTranslation"))
|
||||||
m_strDefaultValue = "_(TOFIX)"; // TODO: wxGetTranslation gives problems to gccxml
|
m_strDefaultValue = "_(TOFIX)"; // TODO: wxGetTranslation gives problems to gccxml
|
||||||
@@ -139,7 +140,10 @@ bool wxArgumentType::operator==(const wxArgumentType& m) const
|
|||||||
if ((const wxType&)(*this) != (const wxType&)m)
|
if ((const wxType&)(*this) != (const wxType&)m)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (m_strDefaultValue != m.m_strDefaultValue)
|
const wxString& def1 = m_strDefaultValueForCmp.IsEmpty() ? m_strDefaultValue : m_strDefaultValueForCmp;
|
||||||
|
const wxString& def2 = m.m_strDefaultValueForCmp.IsEmpty() ? m.m_strDefaultValue : m.m_strDefaultValueForCmp;
|
||||||
|
|
||||||
|
if (def1 != def2)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// we deliberately avoid checks on the argument name
|
// we deliberately avoid checks on the argument name
|
||||||
@@ -170,7 +174,7 @@ bool wxMethod::IsOk() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
wxASSERT((m_bVirtual && m_bPureVirtual) || !m_bVirtual);
|
wxASSERT(!m_bPureVirtual || (m_bPureVirtual && m_bVirtual));
|
||||||
|
|
||||||
for (unsigned int i=0; i<m_args.GetCount(); i++)
|
for (unsigned int i=0; i<m_args.GetCount(); i++)
|
||||||
if (!m_args[i].IsOk()) {
|
if (!m_args[i].IsOk()) {
|
||||||
@@ -403,11 +407,11 @@ void wxXmlInterface::Dump(const wxString& filename)
|
|||||||
bool wxXmlInterface::CheckParseResults() const
|
bool wxXmlInterface::CheckParseResults() const
|
||||||
{
|
{
|
||||||
// this check can be quite slow, so do it only for debug releases:
|
// this check can be quite slow, so do it only for debug releases:
|
||||||
#ifdef __WXDEBUG__
|
//#ifdef __WXDEBUG__
|
||||||
for (unsigned int i=0; i<m_classes.GetCount(); i++)
|
for (unsigned int i=0; i<m_classes.GetCount(); i++)
|
||||||
if (!m_classes[i].CheckConsistency())
|
if (!m_classes[i].CheckConsistency())
|
||||||
return false;
|
return false;
|
||||||
#endif
|
//#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1167,7 @@ bool wxXmlDoxygenInterface::ParseMethod(const wxXmlNode* p, wxMethod& m, wxStrin
|
|||||||
else if (n->GetName() == "declname")
|
else if (n->GetName() == "declname")
|
||||||
namestr = GetTextFromChildren(n);
|
namestr = GetTextFromChildren(n);
|
||||||
else if (n->GetName() == "defval")
|
else if (n->GetName() == "defval")
|
||||||
defstr = GetTextFromChildren(n);
|
defstr = GetTextFromChildren(n).Strip(wxString::both);
|
||||||
else if (n->GetName() == "array")
|
else if (n->GetName() == "array")
|
||||||
arrstr = GetTextFromChildren(n);
|
arrstr = GetTextFromChildren(n);
|
||||||
|
|
||||||
@@ -1175,7 +1179,15 @@ bool wxXmlDoxygenInterface::ParseMethod(const wxXmlNode* p, wxMethod& m, wxStrin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
args.Add(wxArgumentType(typestr + arrstr, defstr, namestr));
|
wxArgumentType newarg(typestr + arrstr, defstr, namestr);
|
||||||
|
|
||||||
|
// can we use preprocessor output to transform the default value
|
||||||
|
// into the same form which gets processed by wxXmlGccInterface?
|
||||||
|
wxStringHashMap::const_iterator it = m_preproc.find(defstr);
|
||||||
|
if (it != m_preproc.end())
|
||||||
|
newarg.SetDefaultValue(defstr, it->second);
|
||||||
|
|
||||||
|
args.Add(newarg);
|
||||||
}
|
}
|
||||||
else if (child->GetName() == "location")
|
else if (child->GetName() == "location")
|
||||||
{
|
{
|
||||||
|
@@ -80,7 +80,7 @@ public:
|
|||||||
wxString GetArgumentName() const
|
wxString GetArgumentName() const
|
||||||
{ return m_strArgName; }
|
{ return m_strArgName; }
|
||||||
|
|
||||||
void SetDefaultValue(const wxString& defval);
|
void SetDefaultValue(const wxString& defval, const wxString& defvalForCmp = wxEmptyString);
|
||||||
wxString GetDefaultValue() const
|
wxString GetDefaultValue() const
|
||||||
{ return m_strDefaultValue; }
|
{ return m_strDefaultValue; }
|
||||||
|
|
||||||
@@ -93,6 +93,11 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxString m_strDefaultValue;
|
wxString m_strDefaultValue;
|
||||||
|
|
||||||
|
// this string may differ from m_strDefaultValue if there were
|
||||||
|
// preprocessor substitutions; can be wxEmptyString.
|
||||||
|
wxString m_strDefaultValueForCmp;
|
||||||
|
|
||||||
wxString m_strArgName; // this one only makes sense when this wxType is
|
wxString m_strArgName; // this one only makes sense when this wxType is
|
||||||
// used as argument type (and not as return type)
|
// used as argument type (and not as return type)
|
||||||
// and can be empty.
|
// and can be empty.
|
||||||
@@ -312,6 +317,8 @@ protected:
|
|||||||
WX_DECLARE_HASH_MAP( unsigned long, wxString,
|
WX_DECLARE_HASH_MAP( unsigned long, wxString,
|
||||||
wxIntegerHash, wxIntegerEqual,
|
wxIntegerHash, wxIntegerEqual,
|
||||||
wxTypeIdHashMap );
|
wxTypeIdHashMap );
|
||||||
|
|
||||||
|
WX_DECLARE_STRING_HASH_MAP( wxString, wxStringHashMap );
|
||||||
#else
|
#else
|
||||||
#include <map>
|
#include <map>
|
||||||
typedef std::basic_string<char> stlString;
|
typedef std::basic_string<char> stlString;
|
||||||
@@ -328,14 +335,6 @@ class wxXmlGccInterface : public wxXmlInterface
|
|||||||
public:
|
public:
|
||||||
wxXmlGccInterface() {}
|
wxXmlGccInterface() {}
|
||||||
|
|
||||||
// !!SPEEDUP-TODO!!
|
|
||||||
// Using wxXmlDocument::Load as is, all the types contained in the
|
|
||||||
// the entire gccXML file are stored in memory while parsing;
|
|
||||||
// however we are only interested to wx's own structs/classes/funcs/etc
|
|
||||||
// so that we could use the file IDs to avoid loading stuff which does
|
|
||||||
// not belong to wx. See the very end of the gccXML file: it contains
|
|
||||||
// a set of <File> nodes referenced by all nodes above.
|
|
||||||
|
|
||||||
bool Parse(const wxString& filename);
|
bool Parse(const wxString& filename);
|
||||||
bool ParseMethod(const wxXmlNode *p,
|
bool ParseMethod(const wxXmlNode *p,
|
||||||
const wxTypeIdHashMap& types,
|
const wxTypeIdHashMap& types,
|
||||||
@@ -363,6 +362,14 @@ public:
|
|||||||
bool Parse(const wxString& filename);
|
bool Parse(const wxString& filename);
|
||||||
bool ParseCompoundDefinition(const wxString& filename);
|
bool ParseCompoundDefinition(const wxString& filename);
|
||||||
bool ParseMethod(const wxXmlNode*, wxMethod&, wxString& header);
|
bool ParseMethod(const wxXmlNode*, wxMethod&, wxString& header);
|
||||||
|
|
||||||
|
// this class can take advantage of the preprocessor output to give
|
||||||
|
// a minor number of false positive warnings in the final comparison
|
||||||
|
void AddPreprocessorValue(const wxString& name, const wxString& val)
|
||||||
|
{ m_preproc[name]=val; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
wxStringHashMap m_preproc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user