// ASEnhancer.cpp // Copyright (c) 2018 by Jim Pattee . // This code is licensed under the MIT License. // License.md describes the conditions under which this software may be distributed. //----------------------------------------------------------------------------- // headers //----------------------------------------------------------------------------- #include "astyle.h" //----------------------------------------------------------------------------- // astyle namespace //----------------------------------------------------------------------------- namespace astyle { // //----------------------------------------------------------------------------- // ASEnhancer class //----------------------------------------------------------------------------- /** * initialize the ASEnhancer. * * init() is called each time an ASFormatter object is initialized. */ void ASEnhancer::init(int _fileType, int _indentLength, int _tabLength, bool _useTabs, bool _forceTab, bool _namespaceIndent, bool _caseIndent, bool _preprocBlockIndent, bool _preprocDefineIndent, bool _emptyLineFill, vector* >* _indentableMacros) { // formatting variables from ASFormatter and ASBeautifier ASBase::init(_fileType); indentLength = _indentLength; tabLength = _tabLength; useTabs = _useTabs; forceTab = _forceTab; namespaceIndent = _namespaceIndent; caseIndent = _caseIndent; preprocBlockIndent = _preprocBlockIndent; preprocDefineIndent = _preprocDefineIndent; emptyLineFill = _emptyLineFill; indentableMacros = _indentableMacros; quoteChar = '\''; // unindent variables lineNumber = 0; braceCount = 0; isInComment = false; isInQuote = false; switchDepth = 0; eventPreprocDepth = 0; lookingForCaseBrace = false; unindentNextLine = false; shouldUnindentLine = false; shouldUnindentComment = false; // switch struct and vector sw.switchBraceCount = 0; sw.unindentDepth = 0; sw.unindentCase = false; switchStack.clear(); // other variables nextLineIsEventIndent = false; isInEventTable = false; nextLineIsDeclareIndent = false; isInDeclareSection = false; } /** * additional formatting for line of source code. * every line of source code in a source code file should be sent * one after the other to this function. * indents event tables * unindents the case blocks * * @param line the original formatted line will be updated if necessary. */ void ASEnhancer::enhance(string& line, bool isInNamespace, bool isInPreprocessor, bool isInSQL) { shouldUnindentLine = true; shouldUnindentComment = false; lineNumber++; // check for beginning of event table if (nextLineIsEventIndent) { isInEventTable = true; nextLineIsEventIndent = false; } // check for beginning of SQL declare section if (nextLineIsDeclareIndent) { isInDeclareSection = true; nextLineIsDeclareIndent = false; } if (line.length() == 0 && !isInEventTable && !isInDeclareSection && !emptyLineFill) return; // test for unindent on attached braces if (unindentNextLine) { sw.unindentDepth++; sw.unindentCase = true; unindentNextLine = false; } // parse characters in the current line parseCurrentLine(line, isInPreprocessor, isInSQL); // check for SQL indentable lines if (isInDeclareSection) { size_t firstText = line.find_first_not_of(" \t"); if (firstText == string::npos || line[firstText] != '#') indentLine(line, 1); } // check for event table indentable lines if (isInEventTable && (eventPreprocDepth == 0 || (namespaceIndent && isInNamespace))) { size_t firstText = line.find_first_not_of(" \t"); if (firstText == string::npos || line[firstText] != '#') indentLine(line, 1); } if (shouldUnindentComment && sw.unindentDepth > 0) unindentLine(line, sw.unindentDepth - 1); else if (shouldUnindentLine && sw.unindentDepth > 0) unindentLine(line, sw.unindentDepth); } /** * convert a force-tab indent to spaces * * @param line a reference to the line that will be converted. */ void ASEnhancer::convertForceTabIndentToSpaces(string& line) const { // replace tab indents with spaces for (size_t i = 0; i < line.length(); i++) { if (!isWhiteSpace(line[i])) break; if (line[i] == '\t') { line.erase(i, 1); line.insert(i, tabLength, ' '); i += tabLength - 1; } } } /** * convert a space indent to force-tab * * @param line a reference to the line that will be converted. */ void ASEnhancer::convertSpaceIndentToForceTab(string& line) const { assert(tabLength > 0); // replace leading spaces with tab indents size_t newSpaceIndentLength = line.find_first_not_of(" \t"); size_t tabCount = newSpaceIndentLength / tabLength; // truncate extra spaces line.replace(0U, tabCount * tabLength, tabCount, '\t'); } /** * find the colon following a 'case' statement * * @param line a reference to the line. * @param caseIndex the line index of the case statement. * @return the line index of the colon. */ size_t ASEnhancer::findCaseColon(const string& line, size_t caseIndex) const { size_t i = caseIndex; bool isInQuote_ = false; char quoteChar_ = ' '; for (; i < line.length(); i++) { if (isInQuote_) { if (line[i] == '\\') { i++; continue; } if (line[i] == quoteChar_) // check ending quote { isInQuote_ = false; quoteChar_ = ' '; continue; } continue; // must close quote before continuing } if (line[i] == '"' // check opening quote || (line[i] == '\'' && !isDigitSeparator(line, i))) { isInQuote_ = true; quoteChar_ = line[i]; continue; } if (line[i] == ':') { if ((i + 1 < line.length()) && (line[i + 1] == ':')) i++; // bypass scope resolution operator else break; // found it } } return i; } /** * indent a line by a given number of tabsets * by inserting leading whitespace to the line argument. * * @param line a reference to the line to indent. * @param indent the number of tabsets to insert. * @return the number of characters inserted. */ int ASEnhancer::indentLine(string& line, int indent) const { if (line.length() == 0 && !emptyLineFill) return 0; size_t charsToInsert = 0; if (forceTab && indentLength != tabLength) { // replace tab indents with spaces convertForceTabIndentToSpaces(line); // insert the space indents charsToInsert = indent * indentLength; line.insert(line.begin(), charsToInsert, ' '); // replace leading spaces with tab indents convertSpaceIndentToForceTab(line); } else if (useTabs) { charsToInsert = indent; line.insert(line.begin(), charsToInsert, '\t'); } else // spaces { charsToInsert = indent * indentLength; line.insert(line.begin(), charsToInsert, ' '); } return charsToInsert; } /** * check for SQL "BEGIN DECLARE SECTION". * must compare case insensitive and allow any spacing between words. * * @param line a reference to the line to indent. * @param index the current line index. * @return true if a hit. */ bool ASEnhancer::isBeginDeclareSectionSQL(const string& line, size_t index) const { string word; size_t hits = 0; size_t i; for (i = index; i < line.length(); i++) { i = line.find_first_not_of(" \t", i); if (i == string::npos) return false; if (line[i] == ';') break; if (!isCharPotentialHeader(line, i)) continue; word = getCurrentWord(line, i); for (char& character : word) character = (char) toupper(character); if (word == "EXEC" || word == "SQL") { i += word.length() - 1; continue; } if (word == "DECLARE" || word == "SECTION") { hits++; i += word.length() - 1; continue; } if (word == "BEGIN") { hits++; i += word.length() - 1; continue; } return false; } if (hits == 3) return true; return false; } /** * check for SQL "END DECLARE SECTION". * must compare case insensitive and allow any spacing between words. * * @param line a reference to the line to indent. * @param index the current line index. * @return true if a hit. */ bool ASEnhancer::isEndDeclareSectionSQL(const string& line, size_t index) const { string word; size_t hits = 0; size_t i; for (i = index; i < line.length(); i++) { i = line.find_first_not_of(" \t", i); if (i == string::npos) return false; if (line[i] == ';') break; if (!isCharPotentialHeader(line, i)) continue; word = getCurrentWord(line, i); for (char& character : word) character = (char) toupper(character); if (word == "EXEC" || word == "SQL") { i += word.length() - 1; continue; } if (word == "DECLARE" || word == "SECTION") { hits++; i += word.length() - 1; continue; } if (word == "END") { hits++; i += word.length() - 1; continue; } return false; } if (hits == 3) return true; return false; } /** * check if a one-line brace has been reached, * i.e. if the currently reached '{' character is closed * with a complimentary '}' elsewhere on the current line, *. * @return false = one-line brace has not been reached. * true = one-line brace has been reached. */ bool ASEnhancer::isOneLineBlockReached(const string& line, int startChar) const { assert(line[startChar] == '{'); bool isInComment_ = false; bool isInQuote_ = false; int _braceCount = 1; int lineLength = line.length(); char quoteChar_ = ' '; char ch = ' '; for (int i = startChar + 1; i < lineLength; ++i) { ch = line[i]; if (isInComment_) { if (line.compare(i, 2, "*/") == 0) { isInComment_ = false; ++i; } continue; } if (ch == '\\') { ++i; continue; } if (isInQuote_) { if (ch == quoteChar_) isInQuote_ = false; continue; } if (ch == '"' || (ch == '\'' && !isDigitSeparator(line, i))) { isInQuote_ = true; quoteChar_ = ch; continue; } if (line.compare(i, 2, "//") == 0) break; if (line.compare(i, 2, "/*") == 0) { isInComment_ = true; ++i; continue; } if (ch == '{') ++_braceCount; else if (ch == '}') --_braceCount; if (_braceCount == 0) return true; } return false; } /** * parse characters in the current line to determine if an indent * or unindent is needed. */ void ASEnhancer::parseCurrentLine(string& line, bool isInPreprocessor, bool isInSQL) { bool isSpecialChar = false; // is a backslash escape character for (size_t i = 0; i < line.length(); i++) { char ch = line[i]; // bypass whitespace if (isWhiteSpace(ch)) continue; // handle special characters (i.e. backslash+character such as \n, \t, ...) if (isSpecialChar) { isSpecialChar = false; continue; } if (!(isInComment) && line.compare(i, 2, "\\\\") == 0) { i++; continue; } if (!(isInComment) && ch == '\\') { isSpecialChar = true; continue; } // handle quotes (such as 'x' and "Hello Dolly") if (!isInComment && (ch == '"' || (ch == '\'' && !isDigitSeparator(line, i)))) { if (!isInQuote) { quoteChar = ch; isInQuote = true; } else if (quoteChar == ch) { isInQuote = false; continue; } } if (isInQuote) continue; // handle comments if (!(isInComment) && line.compare(i, 2, "//") == 0) { // check for windows line markers if (line.compare(i + 2, 1, "\xf0") > 0) lineNumber--; // unindent if not in case braces if (line.find_first_not_of(" \t") == i && sw.switchBraceCount == 1 && sw.unindentCase) shouldUnindentComment = true; break; // finished with the line } if (!(isInComment) && line.compare(i, 2, "/*") == 0) { // unindent if not in case braces if (sw.switchBraceCount == 1 && sw.unindentCase) shouldUnindentComment = true; isInComment = true; size_t commentEnd = line.find("*/", i); if (commentEnd == string::npos) i = line.length() - 1; else i = commentEnd - 1; continue; } if ((isInComment) && line.compare(i, 2, "*/") == 0) { // unindent if not in case braces if (sw.switchBraceCount == 1 && sw.unindentCase) shouldUnindentComment = true; isInComment = false; i++; continue; } if (isInComment) { // unindent if not in case braces if (sw.switchBraceCount == 1 && sw.unindentCase) shouldUnindentComment = true; size_t commentEnd = line.find("*/", i); if (commentEnd == string::npos) i = line.length() - 1; else i = commentEnd - 1; continue; } // if we have reached this far then we are NOT in a comment or string of special characters if (line[i] == '{') braceCount++; if (line[i] == '}') braceCount--; // check for preprocessor within an event table if (isInEventTable && line[i] == '#' && preprocBlockIndent) { string preproc; preproc = line.substr(i + 1); if (preproc.substr(0, 2) == "if") // #if, #ifdef, #ifndef) eventPreprocDepth += 1; if (preproc.substr(0, 5) == "endif" && eventPreprocDepth > 0) eventPreprocDepth -= 1; } bool isPotentialKeyword = isCharPotentialHeader(line, i); // ---------------- wxWidgets and MFC macros ---------------------------------- if (isPotentialKeyword) { for (const auto* indentableMacro : *indentableMacros) { // 'first' is the beginning macro if (findKeyword(line, i, indentableMacro->first)) { nextLineIsEventIndent = true; break; } // 'second' is the ending macro if (findKeyword(line, i, indentableMacro->second)) { isInEventTable = false; eventPreprocDepth = 0; break; } } } // ---------------- process SQL ----------------------------------------------- if (isInSQL) { if (isBeginDeclareSectionSQL(line, i)) nextLineIsDeclareIndent = true; if (isEndDeclareSectionSQL(line, i)) isInDeclareSection = false; break; } // ---------------- process switch statements --------------------------------- if (isPotentialKeyword && findKeyword(line, i, ASResource::AS_SWITCH)) { switchDepth++; switchStack.emplace_back(sw); // save current variables sw.switchBraceCount = 0; sw.unindentCase = false; // don't clear case until end of switch i += 5; // bypass switch statement continue; } // just want unindented case statements from this point if (caseIndent || switchDepth == 0 || (isInPreprocessor && !preprocDefineIndent)) { // bypass the entire word if (isPotentialKeyword) { string name = getCurrentWord(line, i); i += name.length() - 1; } continue; } i = processSwitchBlock(line, i); } // end of for loop * end of for loop * end of for loop * end of for loop } /** * process the character at the current index in a switch block. * * @param line a reference to the line to indent. * @param index the current line index. * @return the new line index. */ size_t ASEnhancer::processSwitchBlock(string& line, size_t index) { size_t i = index; bool isPotentialKeyword = isCharPotentialHeader(line, i); if (line[i] == '{') { sw.switchBraceCount++; if (lookingForCaseBrace) // if 1st after case statement { sw.unindentCase = true; // unindenting this case sw.unindentDepth++; lookingForCaseBrace = false; // not looking now } return i; } lookingForCaseBrace = false; // no opening brace, don't indent if (line[i] == '}') { sw.switchBraceCount--; if (sw.switchBraceCount == 0) // if end of switch statement { int lineUnindent = sw.unindentDepth; if (line.find_first_not_of(" \t") == i && !switchStack.empty()) lineUnindent = switchStack[switchStack.size() - 1].unindentDepth; if (shouldUnindentLine) { if (lineUnindent > 0) i -= unindentLine(line, lineUnindent); shouldUnindentLine = false; } switchDepth--; sw = switchStack.back(); switchStack.pop_back(); } return i; } if (isPotentialKeyword && (findKeyword(line, i, ASResource::AS_CASE) || findKeyword(line, i, ASResource::AS_DEFAULT))) { if (sw.unindentCase) // if unindented last case { sw.unindentCase = false; // stop unindenting previous case sw.unindentDepth--; } i = findCaseColon(line, i); i++; for (; i < line.length(); i++) // bypass whitespace { if (!isWhiteSpace(line[i])) break; } if (i < line.length()) { if (line[i] == '{') { braceCount++; sw.switchBraceCount++; if (!isOneLineBlockReached(line, i)) unindentNextLine = true; return i; } } lookingForCaseBrace = true; i--; // need to process this char return i; } if (isPotentialKeyword) { string name = getCurrentWord(line, i); // bypass the entire name i += name.length() - 1; } return i; } /** * unindent a line by a given number of tabsets * by erasing the leading whitespace from the line argument. * * @param line a reference to the line to unindent. * @param unindent the number of tabsets to erase. * @return the number of characters erased. */ int ASEnhancer::unindentLine(string& line, int unindent) const { size_t whitespace = line.find_first_not_of(" \t"); if (whitespace == string::npos) // if line is blank whitespace = line.length(); // must remove padding, if any if (whitespace == 0) return 0; size_t charsToErase = 0; if (forceTab && indentLength != tabLength) { // replace tab indents with spaces convertForceTabIndentToSpaces(line); // remove the space indents size_t spaceIndentLength = line.find_first_not_of(" \t"); charsToErase = unindent * indentLength; if (charsToErase <= spaceIndentLength) line.erase(0, charsToErase); else charsToErase = 0; // replace leading spaces with tab indents convertSpaceIndentToForceTab(line); } else if (useTabs) { charsToErase = unindent; if (charsToErase <= whitespace) line.erase(0, charsToErase); else charsToErase = 0; } else // spaces { charsToErase = unindent * indentLength; if (charsToErase <= whitespace) line.erase(0, charsToErase); else charsToErase = 0; } return charsToErase; } } // end namespace astyle