Library: Add <container> tag to Libraries, provide configuration for std::vector, std::deque, std::array and STL strings
Token: Added function to jump to the next template argument
This commit is contained in:
parent
eb1c048d2a
commit
e39729ffcc
66
cfg/std.cfg
66
cfg/std.cfg
|
@ -1932,6 +1932,71 @@
|
||||||
<formatstr/>
|
<formatstr/>
|
||||||
</arg>
|
</arg>
|
||||||
</function>
|
</function>
|
||||||
|
|
||||||
|
<container id="stdContainer" endPattern="> !!::">
|
||||||
|
<type templateParameter="0"/>
|
||||||
|
<size>
|
||||||
|
<function name="resize" action="resize"/>
|
||||||
|
<function name="clear" action="clear"/>
|
||||||
|
<function name="size" yields="size"/>
|
||||||
|
<function name="empty" yields="empty"/>
|
||||||
|
</size>
|
||||||
|
<access>
|
||||||
|
<function name="begin" yields="start-iterator"/>
|
||||||
|
<function name="cbegin" yields="start-iterator"/>
|
||||||
|
<function name="rbegin" yields="start-iterator"/>
|
||||||
|
<function name="crbegin" yields="start-iterator"/>
|
||||||
|
<function name="end" yields="end-iterator"/>
|
||||||
|
<function name="cend" yields="end-iterator"/>
|
||||||
|
<function name="rend" yields="end-iterator"/>
|
||||||
|
<function name="crend" yields="end-iterator"/>
|
||||||
|
</access>
|
||||||
|
</container>
|
||||||
|
<container id="stdVectorDeque" startPattern="std :: vector|deque <" inherits="stdContainer">
|
||||||
|
<size>
|
||||||
|
<function name="push_back" action="push"/>
|
||||||
|
<function name="pop_back" action="pop"/>
|
||||||
|
<function name="push_front" action="push"/>
|
||||||
|
<function name="pop_front" action="pop"/>
|
||||||
|
</size>
|
||||||
|
<access indexOperator="array-like">
|
||||||
|
<function name="at" yields="at_index"/>
|
||||||
|
<function name="front" yields="item"/>
|
||||||
|
<function name="back" yields="item"/>
|
||||||
|
<function name="data" yields="buffer"/>
|
||||||
|
</access>
|
||||||
|
</container>
|
||||||
|
<container id="stdArray" startPattern="std :: array <" inherits="stdContainer">
|
||||||
|
<size templateParameter="1"/>
|
||||||
|
<access indexOperator="array-like">
|
||||||
|
<function name="at" yields="at_index"/>
|
||||||
|
<function name="front" yields="item"/>
|
||||||
|
<function name="back" yields="item"/>
|
||||||
|
<function name="data" yields="buffer"/>
|
||||||
|
</access>
|
||||||
|
</container>
|
||||||
|
|
||||||
|
<container id="stdAllString" inherits="stdContainer">
|
||||||
|
<type string="std-like"/>
|
||||||
|
<size>
|
||||||
|
<function name="push_back" action="push"/>
|
||||||
|
<function name="pop_back" action="pop"/>
|
||||||
|
</size>
|
||||||
|
<access indexOperator="array-like">
|
||||||
|
<function name="at" yields="at_index"/>
|
||||||
|
<function name="front" yields="item"/>
|
||||||
|
<function name="back" yields="item"/>
|
||||||
|
<function name="data" yields="buffer"/>
|
||||||
|
<function name="c_str" yields="buffer-nt"/>
|
||||||
|
<function name="length" yields="size"/>
|
||||||
|
</access>
|
||||||
|
</container>
|
||||||
|
<container id="stdBasicString" startPattern="std :: basic_string <" inherits="stdAllString">
|
||||||
|
<type templateParameter="0"/>
|
||||||
|
</container>
|
||||||
|
<container id="stdString" startPattern="std :: string|wstring|u16string|u32string" endPattern="" inherits="stdAllString">
|
||||||
|
</container>
|
||||||
|
|
||||||
<podtype name="int8_t" sign="s" size="1"/>
|
<podtype name="int8_t" sign="s" size="1"/>
|
||||||
<podtype name="int16_t" sign="s" size="2"/>
|
<podtype name="int16_t" sign="s" size="2"/>
|
||||||
<podtype name="int32_t" sign="s" size="4"/>
|
<podtype name="int32_t" sign="s" size="4"/>
|
||||||
|
@ -1950,6 +2015,7 @@
|
||||||
<podtype name="uint_fast64_t" sign="u"/>
|
<podtype name="uint_fast64_t" sign="u"/>
|
||||||
<podtype name="intptr_t" sign="s"/>
|
<podtype name="intptr_t" sign="s"/>
|
||||||
<podtype name="uintptr_t" sign="u"/>
|
<podtype name="uintptr_t" sign="u"/>
|
||||||
|
|
||||||
<!--Not part of standard, but widely supported by runtime libraries-->
|
<!--Not part of standard, but widely supported by runtime libraries-->
|
||||||
<function name="itoa">
|
<function name="itoa">
|
||||||
<noreturn>false</noreturn>
|
<noreturn>false</noreturn>
|
||||||
|
|
122
lib/library.cpp
122
lib/library.cpp
|
@ -388,6 +388,106 @@ Library::Error Library::load(const tinyxml2::XMLDocument &doc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (nodename == "container") {
|
||||||
|
const char* const id = node->Attribute("id");
|
||||||
|
if (!id)
|
||||||
|
return Error(MISSING_ATTRIBUTE, "id");
|
||||||
|
|
||||||
|
Container& container = containers[id];
|
||||||
|
|
||||||
|
const char* const inherits = node->Attribute("inherits");
|
||||||
|
if (inherits) {
|
||||||
|
std::map<std::string, Container>::const_iterator i = containers.find(inherits);
|
||||||
|
if (inherits)
|
||||||
|
container = i->second; // Take values from parent and overwrite them if necessary
|
||||||
|
else
|
||||||
|
return Error(BAD_ATTRIBUTE_VALUE, inherits);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* const startPattern = node->Attribute("startPattern");
|
||||||
|
if (startPattern)
|
||||||
|
container.startPattern = startPattern;
|
||||||
|
const char* const endPattern = node->Attribute("endPattern");
|
||||||
|
if (endPattern)
|
||||||
|
container.endPattern = endPattern;
|
||||||
|
|
||||||
|
for (const tinyxml2::XMLElement *containerNode = node->FirstChildElement(); containerNode; containerNode = containerNode->NextSiblingElement()) {
|
||||||
|
const std::string containerNodeName = containerNode->Name();
|
||||||
|
if (containerNodeName == "size" || containerNodeName == "access" || containerNodeName == "other") {
|
||||||
|
for (const tinyxml2::XMLElement *functionNode = containerNode->FirstChildElement(); functionNode; functionNode = functionNode->NextSiblingElement()) {
|
||||||
|
if (std::string(functionNode->Name()) != "function")
|
||||||
|
return Error(BAD_ELEMENT, functionNode->Name());
|
||||||
|
|
||||||
|
const char* const functionName = functionNode->Attribute("name");
|
||||||
|
if (!functionName)
|
||||||
|
return Error(MISSING_ATTRIBUTE, "name");
|
||||||
|
|
||||||
|
const char* const action_ptr = functionNode->Attribute("action");
|
||||||
|
Container::Action action = Container::NO_ACTION;
|
||||||
|
if (action_ptr) {
|
||||||
|
std::string actionName = action_ptr;
|
||||||
|
if (actionName == "resize")
|
||||||
|
action = Container::RESIZE;
|
||||||
|
else if (actionName == "clear")
|
||||||
|
action = Container::CLEAR;
|
||||||
|
else if (actionName == "push")
|
||||||
|
action = Container::PUSH;
|
||||||
|
else if (actionName == "pop")
|
||||||
|
action = Container::POP;
|
||||||
|
else
|
||||||
|
return Error(BAD_ATTRIBUTE_VALUE, actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* const yield_ptr = functionNode->Attribute("yields");
|
||||||
|
Container::Yield yield = Container::NO_YIELD;
|
||||||
|
if (yield_ptr) {
|
||||||
|
std::string yieldName = yield_ptr;
|
||||||
|
if (yieldName == "at_index")
|
||||||
|
yield = Container::AT_INDEX;
|
||||||
|
else if (yieldName == "item")
|
||||||
|
yield = Container::ITEM;
|
||||||
|
else if (yieldName == "buffer")
|
||||||
|
yield = Container::BUFFER;
|
||||||
|
else if (yieldName == "buffer-nt")
|
||||||
|
yield = Container::BUFFER_NT;
|
||||||
|
else if (yieldName == "start-iterator")
|
||||||
|
yield = Container::START_ITERATOR;
|
||||||
|
else if (yieldName == "end-iterator")
|
||||||
|
yield = Container::END_ITERATOR;
|
||||||
|
else if (yieldName == "size")
|
||||||
|
yield = Container::SIZE;
|
||||||
|
else if (yieldName == "empty")
|
||||||
|
yield = Container::EMPTY;
|
||||||
|
else
|
||||||
|
return Error(BAD_ATTRIBUTE_VALUE, yieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.functions[functionName].action = action;
|
||||||
|
container.functions[functionName].yield = yield;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerNodeName == "size") {
|
||||||
|
const char* const templateArg = containerNode->Attribute("templateParameter");
|
||||||
|
if (templateArg)
|
||||||
|
container.size_templateArgNo = atoi(templateArg);
|
||||||
|
} else if (containerNodeName == "access") {
|
||||||
|
const char* const indexArg = containerNode->Attribute("indexOperator");
|
||||||
|
if (indexArg)
|
||||||
|
container.arrayLike_indexOp = std::string(indexArg) == "array-like";
|
||||||
|
}
|
||||||
|
} else if (containerNodeName == "type") {
|
||||||
|
const char* const templateArg = containerNode->Attribute("templateParameter");
|
||||||
|
if (templateArg)
|
||||||
|
container.type_templateArgNo = atoi(templateArg);
|
||||||
|
|
||||||
|
const char* const string = containerNode->Attribute("string");
|
||||||
|
if (string)
|
||||||
|
container.stdStringLike = std::string(string) == "std-like";
|
||||||
|
} else
|
||||||
|
return Error(BAD_ELEMENT, containerNodeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else if (nodename == "podtype") {
|
else if (nodename == "podtype") {
|
||||||
const char * const name = node->Attribute("name");
|
const char * const name = node->Attribute("name");
|
||||||
if (!name)
|
if (!name)
|
||||||
|
@ -535,3 +635,25 @@ bool Library::isScopeNoReturn(const Token *end, std::string *unknownFunc) const
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Library::Container* Library::detectContainer(const Token* typeStart) const
|
||||||
|
{
|
||||||
|
for (std::map<std::string, Container>::const_iterator i = containers.begin(); i != containers.end(); ++i) {
|
||||||
|
const Container& container = i->second;
|
||||||
|
if (container.startPattern.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Token::Match(typeStart, container.startPattern.c_str())) {
|
||||||
|
if (container.endPattern.empty())
|
||||||
|
return &container;
|
||||||
|
|
||||||
|
for (const Token* tok = typeStart; tok && !tok->varId(); tok = tok->next()) {
|
||||||
|
if (tok->link()) {
|
||||||
|
if (Token::Match(tok->link(), container.endPattern.c_str()))
|
||||||
|
return &container;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
|
@ -137,6 +137,51 @@ public:
|
||||||
|
|
||||||
bool isScopeNoReturn(const Token *end, std::string *unknownFunc) const;
|
bool isScopeNoReturn(const Token *end, std::string *unknownFunc) const;
|
||||||
|
|
||||||
|
class Container {
|
||||||
|
public:
|
||||||
|
Container() :
|
||||||
|
type_templateArgNo(-1),
|
||||||
|
size_templateArgNo(-1),
|
||||||
|
arrayLike_indexOp(false),
|
||||||
|
stdStringLike(false) {
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action {
|
||||||
|
RESIZE, CLEAR, PUSH, POP,
|
||||||
|
NO_ACTION
|
||||||
|
};
|
||||||
|
enum Yield {
|
||||||
|
AT_INDEX, ITEM, BUFFER, BUFFER_NT, START_ITERATOR, END_ITERATOR, SIZE, EMPTY,
|
||||||
|
NO_YIELD
|
||||||
|
};
|
||||||
|
struct Function {
|
||||||
|
Action action;
|
||||||
|
Yield yield;
|
||||||
|
};
|
||||||
|
std::string startPattern, endPattern;
|
||||||
|
std::map<std::string, Function> functions;
|
||||||
|
int type_templateArgNo;
|
||||||
|
int size_templateArgNo;
|
||||||
|
bool arrayLike_indexOp;
|
||||||
|
bool stdStringLike;
|
||||||
|
|
||||||
|
Action getAction(const std::string& function) const {
|
||||||
|
std::map<std::string, Function>::const_iterator i = functions.find(function);
|
||||||
|
if (i != functions.end())
|
||||||
|
return i->second.action;
|
||||||
|
return NO_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
Yield getYield(const std::string& function) const {
|
||||||
|
std::map<std::string, Function>::const_iterator i = functions.find(function);
|
||||||
|
if (i != functions.end())
|
||||||
|
return i->second.yield;
|
||||||
|
return NO_YIELD;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::map<std::string, Container> containers;
|
||||||
|
const Container* detectContainer(const Token* typeStart) const;
|
||||||
|
|
||||||
class ArgumentChecks {
|
class ArgumentChecks {
|
||||||
public:
|
public:
|
||||||
ArgumentChecks() :
|
ArgumentChecks() :
|
||||||
|
|
|
@ -788,6 +788,19 @@ Token* Token::nextArgumentBeforeCreateLinks2() const
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Token* Token::nextTemplateArgument() const
|
||||||
|
{
|
||||||
|
for (const Token* tok = this; tok; tok = tok->next()) {
|
||||||
|
if (tok->str() == ",")
|
||||||
|
return tok->next();
|
||||||
|
else if (tok->link() && Token::Match(tok, "(|{|[|<"))
|
||||||
|
tok = tok->link();
|
||||||
|
else if (Token::Match(tok, ">|;"))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const Token * Token::findClosingBracket() const
|
const Token * Token::findClosingBracket() const
|
||||||
{
|
{
|
||||||
const Token *closing = nullptr;
|
const Token *closing = nullptr;
|
||||||
|
|
|
@ -649,6 +649,13 @@ public:
|
||||||
*/
|
*/
|
||||||
Token* nextArgumentBeforeCreateLinks2() const;
|
Token* nextArgumentBeforeCreateLinks2() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the first token of the next template argument. Does only work on template argument
|
||||||
|
* lists. Requires that Tokenizer::createLinks2() has been called before.
|
||||||
|
* Returns 0, if there is no next argument.
|
||||||
|
*/
|
||||||
|
Token* nextTemplateArgument() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the closing bracket of opening '<'. Should only be used if link()
|
* Returns the closing bracket of opening '<'. Should only be used if link()
|
||||||
* is unavailable.
|
* is unavailable.
|
||||||
|
|
|
@ -920,6 +920,32 @@ Checking unusedvar.cpp...
|
||||||
[unusedvar.cpp:2]: (style) Unused variable: a</programlisting>
|
[unusedvar.cpp:2]: (style) Unused variable: a</programlisting>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<title>container</title>
|
||||||
|
|
||||||
|
<para>A lot of C++ libraries, among those the STL itself, provide containers with very similar functionality. Libraries can be used to tell cppcheck about their behaviour. Each container needs a unique ID. It can optionally have a startPattern, which must be a valid Token::Match pattern and an endPattern that is compared to the linked token of the first token with such a link. The optional attribute "inherits" takes an ID from a previously defined container.</para>
|
||||||
|
|
||||||
|
<para>Inside the <container> tag, functions can be defined inside of the tags <size>, <access> and <other> (on your choice). Each of them can specify an action like "resize" and/or the result it yields, for example "end-iterator".</para>
|
||||||
|
|
||||||
|
<para>The following example provides a definition for std::vector, based on the definition of "stdContainer" (not shown):</para>
|
||||||
|
|
||||||
|
<programlisting><?xml version="1.0"?>
|
||||||
|
<def>
|
||||||
|
<container id="stdVector" startPattern="std :: vector &lt;" inherits="stdContainer">
|
||||||
|
<size>
|
||||||
|
<function name="push_back" action="push"/>
|
||||||
|
<function name="pop_back" action="pop"/>
|
||||||
|
</size>
|
||||||
|
<access indexOperator="array-like">
|
||||||
|
<function name="at" yields="at_index"/>
|
||||||
|
<function name="front" yields="fixed"/>
|
||||||
|
<function name="back" yields="fixed"/>
|
||||||
|
</access>
|
||||||
|
</container>
|
||||||
|
</def></programlisting>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<title>Example configuration for strcpy()</title>
|
<title>Example configuration for strcpy()</title>
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ private:
|
||||||
TEST_CASE(memory2); // define extra "free" allocation functions
|
TEST_CASE(memory2); // define extra "free" allocation functions
|
||||||
TEST_CASE(resource);
|
TEST_CASE(resource);
|
||||||
TEST_CASE(podtype);
|
TEST_CASE(podtype);
|
||||||
|
TEST_CASE(container);
|
||||||
TEST_CASE(version);
|
TEST_CASE(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +277,80 @@ private:
|
||||||
ASSERT_EQUALS(0, type ? type->sign : '?');
|
ASSERT_EQUALS(0, type ? type->sign : '?');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void container() const {
|
||||||
|
const char xmldata[] = "<?xml version=\"1.0\"?>\n"
|
||||||
|
"<def>\n"
|
||||||
|
" <container id=\"A\" startPattern=\"std :: A <\" endPattern=\"> !!::\">\n"
|
||||||
|
" <type templateParameter=\"1\"/>\n"
|
||||||
|
" <size templateParameter=\"4\">\n"
|
||||||
|
" <function name=\"resize\" action=\"resize\"/>\n"
|
||||||
|
" <function name=\"clear\" action=\"clear\"/>\n"
|
||||||
|
" <function name=\"size\" yields=\"size\"/>\n"
|
||||||
|
" <function name=\"empty\" yields=\"empty\"/>\n"
|
||||||
|
" <function name=\"push_back\" action=\"push\"/>\n"
|
||||||
|
" <function name=\"pop_back\" action=\"pop\"/>\n"
|
||||||
|
" </size>\n"
|
||||||
|
" <access>\n"
|
||||||
|
" <function name=\"at\" yields=\"at_index\"/>\n"
|
||||||
|
" <function name=\"begin\" yields=\"start-iterator\"/>\n"
|
||||||
|
" <function name=\"end\" yields=\"end-iterator\"/>\n"
|
||||||
|
" <function name=\"data\" yields=\"buffer\"/>\n"
|
||||||
|
" <function name=\"c_str\" yields=\"buffer-nt\"/>\n"
|
||||||
|
" <function name=\"front\" yields=\"item\"/>\n"
|
||||||
|
" </access>\n"
|
||||||
|
" </container>\n"
|
||||||
|
" <container id=\"B\" startPattern=\"std :: B <\" inherits=\"A\">\n"
|
||||||
|
" <size templateParameter=\"3\"/>\n" // Inherits all but templateParameter
|
||||||
|
" </container>\n"
|
||||||
|
" <container id=\"C\">\n"
|
||||||
|
" <type string=\"std-like\"/>\n"
|
||||||
|
" <access indexOperator=\"array-like\"/>\n"
|
||||||
|
" </container>\n"
|
||||||
|
"</def>";
|
||||||
|
tinyxml2::XMLDocument doc;
|
||||||
|
doc.Parse(xmldata, sizeof(xmldata));
|
||||||
|
|
||||||
|
Library library;
|
||||||
|
library.load(doc);
|
||||||
|
|
||||||
|
Library::Container& A = library.containers["A"];
|
||||||
|
Library::Container& B = library.containers["B"];
|
||||||
|
Library::Container& C = library.containers["C"];
|
||||||
|
|
||||||
|
ASSERT_EQUALS(A.type_templateArgNo, 1);
|
||||||
|
ASSERT_EQUALS(A.size_templateArgNo, 4);
|
||||||
|
ASSERT_EQUALS(A.startPattern, "std :: A <");
|
||||||
|
ASSERT_EQUALS(A.endPattern, "> !!::");
|
||||||
|
ASSERT_EQUALS(A.stdStringLike, false);
|
||||||
|
ASSERT_EQUALS(A.arrayLike_indexOp, false);
|
||||||
|
ASSERT_EQUALS(Library::Container::SIZE, A.getYield("size"));
|
||||||
|
ASSERT_EQUALS(Library::Container::EMPTY, A.getYield("empty"));
|
||||||
|
ASSERT_EQUALS(Library::Container::AT_INDEX, A.getYield("at"));
|
||||||
|
ASSERT_EQUALS(Library::Container::START_ITERATOR, A.getYield("begin"));
|
||||||
|
ASSERT_EQUALS(Library::Container::END_ITERATOR, A.getYield("end"));
|
||||||
|
ASSERT_EQUALS(Library::Container::BUFFER, A.getYield("data"));
|
||||||
|
ASSERT_EQUALS(Library::Container::BUFFER_NT, A.getYield("c_str"));
|
||||||
|
ASSERT_EQUALS(Library::Container::ITEM, A.getYield("front"));
|
||||||
|
ASSERT_EQUALS(Library::Container::NO_YIELD, A.getYield("foo"));
|
||||||
|
ASSERT_EQUALS(Library::Container::RESIZE, A.getAction("resize"));
|
||||||
|
ASSERT_EQUALS(Library::Container::CLEAR, A.getAction("clear"));
|
||||||
|
ASSERT_EQUALS(Library::Container::PUSH, A.getAction("push_back"));
|
||||||
|
ASSERT_EQUALS(Library::Container::POP, A.getAction("pop_back"));
|
||||||
|
ASSERT_EQUALS(Library::Container::NO_ACTION, A.getAction("foo"));
|
||||||
|
|
||||||
|
ASSERT_EQUALS(B.type_templateArgNo, 1);
|
||||||
|
ASSERT_EQUALS(B.size_templateArgNo, 3);
|
||||||
|
ASSERT_EQUALS(B.startPattern, "std :: B <");
|
||||||
|
ASSERT_EQUALS(B.endPattern, "> !!::");
|
||||||
|
ASSERT_EQUALS(B.functions.size(), A.functions.size());
|
||||||
|
|
||||||
|
ASSERT(C.functions.empty());
|
||||||
|
ASSERT_EQUALS(C.type_templateArgNo, -1);
|
||||||
|
ASSERT_EQUALS(C.size_templateArgNo, -1);
|
||||||
|
ASSERT_EQUALS(C.stdStringLike, true);
|
||||||
|
ASSERT_EQUALS(C.arrayLike_indexOp, true);
|
||||||
|
}
|
||||||
|
|
||||||
void version() const {
|
void version() const {
|
||||||
{
|
{
|
||||||
const char xmldata [] = "<?xml version=\"1.0\"?>\n"
|
const char xmldata [] = "<?xml version=\"1.0\"?>\n"
|
||||||
|
|
Loading…
Reference in New Issue