1 // ASEnhancer.cpp
2 // Copyright (c) 2018 by Jim Pattee <jimp03@email.com>.
3 // This code is licensed under the MIT License.
4 // License.md describes the conditions under which this software may be distributed.
5 
6 //-----------------------------------------------------------------------------
7 // headers
8 //-----------------------------------------------------------------------------
9 
10 #include "astyle/astyle.h"
11 
12 //-----------------------------------------------------------------------------
13 // astyle namespace
14 //-----------------------------------------------------------------------------
15 
16 namespace astyle {
17 //
18 //-----------------------------------------------------------------------------
19 // ASEnhancer class
20 //-----------------------------------------------------------------------------
21 
22 /**
23  * initialize the ASEnhancer.
24  *
25  * init() is called each time an ASFormatter object is initialized.
26  */
init(int _fileType,int _indentLength,int _tabLength,bool _useTabs,bool _forceTab,bool _namespaceIndent,bool _caseIndent,bool _preprocBlockIndent,bool _preprocDefineIndent,bool _emptyLineFill,vector<const pair<const string,const string> * > * _indentableMacros)27 void ASEnhancer::init(int  _fileType,
28                       int  _indentLength,
29                       int  _tabLength,
30                       bool _useTabs,
31                       bool _forceTab,
32                       bool _namespaceIndent,
33                       bool _caseIndent,
34                       bool _preprocBlockIndent,
35                       bool _preprocDefineIndent,
36                       bool _emptyLineFill,
37                       vector<const pair<const string, const string>* >* _indentableMacros)
38 {
39 	// formatting variables from ASFormatter and ASBeautifier
40 	ASBase::init(_fileType);
41 	indentLength = _indentLength;
42 	tabLength = _tabLength;
43 	useTabs = _useTabs;
44 	forceTab = _forceTab;
45 	namespaceIndent = _namespaceIndent;
46 	caseIndent = _caseIndent;
47 	preprocBlockIndent = _preprocBlockIndent;
48 	preprocDefineIndent = _preprocDefineIndent;
49 	emptyLineFill = _emptyLineFill;
50 	indentableMacros = _indentableMacros;
51 	quoteChar = '\'';
52 
53 	// unindent variables
54 	lineNumber = 0;
55 	braceCount = 0;
56 	isInComment = false;
57 	isInQuote = false;
58 	switchDepth = 0;
59 	eventPreprocDepth = 0;
60 	lookingForCaseBrace = false;
61 	unindentNextLine = false;
62 	shouldUnindentLine = false;
63 	shouldUnindentComment = false;
64 
65 	// switch struct and vector
66 	sw.switchBraceCount = 0;
67 	sw.unindentDepth = 0;
68 	sw.unindentCase = false;
69 	switchStack.clear();
70 
71 	// other variables
72 	nextLineIsEventIndent = false;
73 	isInEventTable = false;
74 	nextLineIsDeclareIndent = false;
75 	isInDeclareSection = false;
76 }
77 
78 /**
79  * additional formatting for line of source code.
80  * every line of source code in a source code file should be sent
81  *     one after the other to this function.
82  * indents event tables
83  * unindents the case blocks
84  *
85  * @param line       the original formatted line will be updated if necessary.
86  */
enhance(string & line,bool isInNamespace,bool isInPreprocessor,bool isInSQL)87 void ASEnhancer::enhance(string& line, bool isInNamespace, bool isInPreprocessor, bool isInSQL)
88 {
89 	shouldUnindentLine = true;
90 	shouldUnindentComment = false;
91 	lineNumber++;
92 
93 	// check for beginning of event table
94 	if (nextLineIsEventIndent)
95 	{
96 		isInEventTable = true;
97 		nextLineIsEventIndent = false;
98 	}
99 
100 	// check for beginning of SQL declare section
101 	if (nextLineIsDeclareIndent)
102 	{
103 		isInDeclareSection = true;
104 		nextLineIsDeclareIndent = false;
105 	}
106 
107 	if (line.length() == 0
108 	        && !isInEventTable
109 	        && !isInDeclareSection
110 	        && !emptyLineFill)
111 		return;
112 
113 	// test for unindent on attached braces
114 	if (unindentNextLine)
115 	{
116 		sw.unindentDepth++;
117 		sw.unindentCase = true;
118 		unindentNextLine = false;
119 	}
120 
121 	// parse characters in the current line
122 	parseCurrentLine(line, isInPreprocessor, isInSQL);
123 
124 	// check for SQL indentable lines
125 	if (isInDeclareSection)
126 	{
127 		size_t firstText = line.find_first_not_of(" \t");
128 		if (firstText == string::npos || line[firstText] != '#')
129 			indentLine(line, 1);
130 	}
131 
132 	// check for event table indentable lines
133 	if (isInEventTable
134 	        && (eventPreprocDepth == 0
135 	            || (namespaceIndent && isInNamespace)))
136 	{
137 		size_t firstText = line.find_first_not_of(" \t");
138 		if (firstText == string::npos || line[firstText] != '#')
139 			indentLine(line, 1);
140 	}
141 
142 	if (shouldUnindentComment && sw.unindentDepth > 0)
143 		unindentLine(line, sw.unindentDepth - 1);
144 	else if (shouldUnindentLine && sw.unindentDepth > 0)
145 		unindentLine(line, sw.unindentDepth);
146 }
147 
148 /**
149  * convert a force-tab indent to spaces
150  *
151  * @param line          a reference to the line that will be converted.
152  */
convertForceTabIndentToSpaces(string & line) const153 void ASEnhancer::convertForceTabIndentToSpaces(string& line) const
154 {
155 	// replace tab indents with spaces
156 	for (size_t i = 0; i < line.length(); i++)
157 	{
158 		if (!isWhiteSpace(line[i]))
159 			break;
160 		if (line[i] == '\t')
161 		{
162 			line.erase(i, 1);
163 			line.insert(i, tabLength, ' ');
164 			i += tabLength - 1;
165 		}
166 	}
167 }
168 
169 /**
170  * convert a space indent to force-tab
171  *
172  * @param line          a reference to the line that will be converted.
173  */
convertSpaceIndentToForceTab(string & line) const174 void ASEnhancer::convertSpaceIndentToForceTab(string& line) const
175 {
176 	assert(tabLength > 0);
177 
178 	// replace leading spaces with tab indents
179 	size_t newSpaceIndentLength = line.find_first_not_of(" \t");
180 	size_t tabCount = newSpaceIndentLength / tabLength;		// truncate extra spaces
181 	line.replace(0U, tabCount * tabLength, tabCount, '\t');
182 }
183 
184 /**
185  * find the colon following a 'case' statement
186  *
187  * @param line          a reference to the line.
188  * @param caseIndex     the line index of the case statement.
189  * @return              the line index of the colon.
190  */
findCaseColon(const string & line,size_t caseIndex) const191 size_t ASEnhancer::findCaseColon(const string& line, size_t caseIndex) const
192 {
193 	size_t i = caseIndex;
194 	bool isInQuote_ = false;
195 	char quoteChar_ = ' ';
196 	for (; i < line.length(); i++)
197 	{
198 		if (isInQuote_)
199 		{
200 			if (line[i] == '\\')
201 			{
202 				i++;
203 				continue;
204 			}
205 			if (line[i] == quoteChar_)          // check ending quote
206 			{
207 				isInQuote_ = false;
208 				quoteChar_ = ' ';
209 				continue;
210 			}
211 				continue;                           // must close quote before continuing
212 			}
213 		if (line[i] == '"' 		// check opening quote
214 		        || (line[i] == '\'' && !isDigitSeparator(line, i)))
215 		{
216 			isInQuote_ = true;
217 			quoteChar_ = line[i];
218 			continue;
219 		}
220 		if (line[i] == ':')
221 		{
222 			if ((i + 1 < line.length()) && (line[i + 1] == ':'))
223 				i++;                                // bypass scope resolution operator
224 			else
225 				break;                              // found it
226 		}
227 	}
228 	return i;
229 }
230 
231 /**
232 * indent a line by a given number of tabsets
233  *    by inserting leading whitespace to the line argument.
234  *
235  * @param line          a reference to the line to indent.
236  * @param indent        the number of tabsets to insert.
237  * @return              the number of characters inserted.
238  */
indentLine(string & line,int indent) const239 int ASEnhancer::indentLine(string& line, int indent) const
240 {
241 	if (line.length() == 0
242 	        && !emptyLineFill)
243 		return 0;
244 
245 	size_t charsToInsert = 0;
246 
247 	if (forceTab && indentLength != tabLength)
248 	{
249 		// replace tab indents with spaces
250 		convertForceTabIndentToSpaces(line);
251 		// insert the space indents
252 		charsToInsert = indent * indentLength;
253 		line.insert(line.begin(), charsToInsert, ' ');
254 		// replace leading spaces with tab indents
255 		convertSpaceIndentToForceTab(line);
256 	}
257 	else if (useTabs)
258 	{
259 		charsToInsert = indent;
260 		line.insert(line.begin(), charsToInsert, '\t');
261 	}
262 	else // spaces
263 	{
264 		charsToInsert = indent * indentLength;
265 		line.insert(line.begin(), charsToInsert, ' ');
266 	}
267 
268 	return charsToInsert;
269 }
270 
271 /**
272  * check for SQL "BEGIN DECLARE SECTION".
273  * must compare case insensitive and allow any spacing between words.
274  *
275  * @param line          a reference to the line to indent.
276  * @param index         the current line index.
277  * @return              true if a hit.
278  */
isBeginDeclareSectionSQL(const string & line,size_t index) const279 bool ASEnhancer::isBeginDeclareSectionSQL(const string& line, size_t index) const
280 {
281 	string word;
282 	size_t hits = 0;
283 	size_t i;
284 	for (i = index; i < line.length(); i++)
285 	{
286 		i = line.find_first_not_of(" \t", i);
287 		if (i == string::npos)
288 			return false;
289 		if (line[i] == ';')
290 			break;
291 		if (!isCharPotentialHeader(line, i))
292 			continue;
293 		word = getCurrentWord(line, i);
294 		for (char& character : word)
295 			character = (char) toupper(character);
296 		if (word == "EXEC" || word == "SQL")
297 		{
298 			i += word.length() - 1;
299 			continue;
300 		}
301 		if (word == "DECLARE" || word == "SECTION")
302 		{
303 			hits++;
304 			i += word.length() - 1;
305 			continue;
306 		}
307 		if (word == "BEGIN")
308 		{
309 			hits++;
310 			i += word.length() - 1;
311 			continue;
312 		}
313 		return false;
314 	}
315 	if (hits == 3)
316 		return true;
317 	return false;
318 }
319 
320 /**
321  * check for SQL "END DECLARE SECTION".
322  * must compare case insensitive and allow any spacing between words.
323  *
324  * @param line          a reference to the line to indent.
325  * @param index         the current line index.
326  * @return              true if a hit.
327  */
isEndDeclareSectionSQL(const string & line,size_t index) const328 bool ASEnhancer::isEndDeclareSectionSQL(const string& line, size_t index) const
329 {
330 	string word;
331 	size_t hits = 0;
332 	size_t i;
333 	for (i = index; i < line.length(); i++)
334 	{
335 		i = line.find_first_not_of(" \t", i);
336 		if (i == string::npos)
337 			return false;
338 		if (line[i] == ';')
339 			break;
340 		if (!isCharPotentialHeader(line, i))
341 			continue;
342 		word = getCurrentWord(line, i);
343 		for (char& character : word)
344 			character = (char) toupper(character);
345 		if (word == "EXEC" || word == "SQL")
346 		{
347 			i += word.length() - 1;
348 			continue;
349 		}
350 		if (word == "DECLARE" || word == "SECTION")
351 		{
352 			hits++;
353 			i += word.length() - 1;
354 			continue;
355 		}
356 		if (word == "END")
357 		{
358 			hits++;
359 			i += word.length() - 1;
360 			continue;
361 		}
362 		return false;
363 	}
364 	if (hits == 3)
365 		return true;
366 	return false;
367 }
368 
369 /**
370  * check if a one-line brace has been reached,
371  * i.e. if the currently reached '{' character is closed
372  * with a complimentary '}' elsewhere on the current line,
373  *.
374  * @return     false = one-line brace has not been reached.
375  *             true  = one-line brace has been reached.
376  */
isOneLineBlockReached(const string & line,int startChar) const377 bool ASEnhancer::isOneLineBlockReached(const string& line, int startChar) const
378 {
379 	assert(line[startChar] == '{');
380 
381 	bool isInComment_ = false;
382 	bool isInQuote_ = false;
383 	int _braceCount = 1;
384 	int lineLength = line.length();
385 	char quoteChar_ = ' ';
386 	char ch = ' ';
387 
388 	for (int i = startChar + 1; i < lineLength; ++i)
389 	{
390 		ch = line[i];
391 
392 		if (isInComment_)
393 		{
394 			if (line.compare(i, 2, "*/") == 0)
395 			{
396 				isInComment_ = false;
397 				++i;
398 			}
399 			continue;
400 		}
401 
402 		if (ch == '\\')
403 		{
404 			++i;
405 			continue;
406 		}
407 
408 		if (isInQuote_)
409 		{
410 			if (ch == quoteChar_)
411 				isInQuote_ = false;
412 			continue;
413 		}
414 
415 		if (ch == '"'
416 		        || (ch == '\'' && !isDigitSeparator(line, i)))
417 		{
418 			isInQuote_ = true;
419 			quoteChar_ = ch;
420 			continue;
421 		}
422 
423 		if (line.compare(i, 2, "//") == 0)
424 			break;
425 
426 		if (line.compare(i, 2, "/*") == 0)
427 		{
428 			isInComment_ = true;
429 			++i;
430 			continue;
431 		}
432 
433 		if (ch == '{')
434 			++_braceCount;
435 		else if (ch == '}')
436 			--_braceCount;
437 
438 		if (_braceCount == 0)
439 			return true;
440 	}
441 
442 	return false;
443 }
444 
445 /**
446  * parse characters in the current line to determine if an indent
447  * or unindent is needed.
448  */
parseCurrentLine(string & line,bool isInPreprocessor,bool isInSQL)449 void ASEnhancer::parseCurrentLine(string& line, bool isInPreprocessor, bool isInSQL)
450 {
451 	bool isSpecialChar = false;			// is a backslash escape character
452 
453 	for (size_t i = 0; i < line.length(); i++)
454 	{
455 		char ch = line[i];
456 
457 		// bypass whitespace
458 		if (isWhiteSpace(ch))
459 			continue;
460 
461 		// handle special characters (i.e. backslash+character such as \n, \t, ...)
462 		if (isSpecialChar)
463 		{
464 			isSpecialChar = false;
465 			continue;
466 		}
467 		if (!(isInComment) && line.compare(i, 2, "\\\\") == 0)
468 		{
469 			i++;
470 			continue;
471 		}
472 		if (!(isInComment) && ch == '\\')
473 		{
474 			isSpecialChar = true;
475 			continue;
476 		}
477 
478 		// handle quotes (such as 'x' and "Hello Dolly")
479 		if (!isInComment
480 		        && (ch == '"'
481 		            || (ch == '\'' && !isDigitSeparator(line, i))))
482 		{
483 			if (!isInQuote)
484 			{
485 				quoteChar = ch;
486 				isInQuote = true;
487 			}
488 			else if (quoteChar == ch)
489 			{
490 				isInQuote = false;
491 				continue;
492 			}
493 		}
494 
495 		if (isInQuote)
496 			continue;
497 
498 		// handle comments
499 
500 		if (!(isInComment) && line.compare(i, 2, "//") == 0)
501 		{
502 			// check for windows line markers
503 			if (line.compare(i + 2, 1, "\xf0") > 0)
504 				lineNumber--;
505 			// unindent if not in case braces
506 			if (line.find_first_not_of(" \t") == i
507 			        && sw.switchBraceCount == 1
508 			        && sw.unindentCase)
509 				shouldUnindentComment = true;
510 			break;                 // finished with the line
511 		}
512 		if (!(isInComment) && line.compare(i, 2, "/*") == 0)
513 		{
514 			// unindent if not in case braces
515 			if (sw.switchBraceCount == 1 && sw.unindentCase)
516 				shouldUnindentComment = true;
517 			isInComment = true;
518 			size_t commentEnd = line.find("*/", i);
519 			if (commentEnd == string::npos)
520 				i = line.length() - 1;
521 			else
522 				i = commentEnd - 1;
523 			continue;
524 		}
525 		if ((isInComment) && line.compare(i, 2, "*/") == 0)
526 		{
527 			// unindent if not in case braces
528 			if (sw.switchBraceCount == 1 && sw.unindentCase)
529 				shouldUnindentComment = true;
530 			isInComment = false;
531 			i++;
532 			continue;
533 		}
534 		if (isInComment)
535 		{
536 			// unindent if not in case braces
537 			if (sw.switchBraceCount == 1 && sw.unindentCase)
538 				shouldUnindentComment = true;
539 			size_t commentEnd = line.find("*/", i);
540 			if (commentEnd == string::npos)
541 				i = line.length() - 1;
542 			else
543 				i = commentEnd - 1;
544 			continue;
545 		}
546 
547 		// if we have reached this far then we are NOT in a comment or string of special characters
548 
549 		if (line[i] == '{')
550 			braceCount++;
551 
552 		if (line[i] == '}')
553 			braceCount--;
554 
555 		// check for preprocessor within an event table
556 		if (isInEventTable && line[i] == '#' && preprocBlockIndent)
557 		{
558 			string preproc;
559 			preproc = line.substr(i + 1);
560 			if (preproc.substr(0, 2) == "if") // #if, #ifdef, #ifndef)
561 				eventPreprocDepth += 1;
562 			if (preproc.substr(0, 5) == "endif" && eventPreprocDepth > 0)
563 				eventPreprocDepth -= 1;
564 		}
565 
566 		bool isPotentialKeyword = isCharPotentialHeader(line, i);
567 
568 		// ----------------  wxWidgets and MFC macros  ----------------------------------
569 
570 		if (isPotentialKeyword)
571 		{
572 			for (const auto* indentableMacro : *indentableMacros)
573 			{
574 				// 'first' is the beginning macro
575 				if (findKeyword(line, i, indentableMacro->first))
576 				{
577 					nextLineIsEventIndent = true;
578 					break;
579 				}
580 				// 'second' is the ending macro
581 				if (findKeyword(line, i, indentableMacro->second))
582 				{
583 					isInEventTable = false;
584 					eventPreprocDepth = 0;
585 					break;
586 				}
587 			}
588 		}
589 
590 		// ----------------  process SQL  -----------------------------------------------
591 
592 		if (isInSQL)
593 		{
594 			if (isBeginDeclareSectionSQL(line, i))
595 				nextLineIsDeclareIndent = true;
596 			if (isEndDeclareSectionSQL(line, i))
597 				isInDeclareSection = false;
598 			break;
599 		}
600 
601 		// ----------------  process switch statements  ---------------------------------
602 
603 		if (isPotentialKeyword && findKeyword(line, i, ASResource::AS_SWITCH))
604 		{
605 			switchDepth++;
606 			switchStack.emplace_back(sw);                      // save current variables
607 			sw.switchBraceCount = 0;
608 			sw.unindentCase = false;                        // don't clear case until end of switch
609 			i += 5;                                         // bypass switch statement
610 			continue;
611 		}
612 
613 		// just want unindented case statements from this point
614 
615 		if (caseIndent
616 		        || switchDepth == 0
617 		        || (isInPreprocessor && !preprocDefineIndent))
618 		{
619 			// bypass the entire word
620 			if (isPotentialKeyword)
621 			{
622 				string name = getCurrentWord(line, i);
623 				i += name.length() - 1;
624 			}
625 			continue;
626 		}
627 
628 		i = processSwitchBlock(line, i);
629 
630 	}   // end of for loop * end of for loop * end of for loop * end of for loop
631 }
632 
633 /**
634  * process the character at the current index in a switch block.
635  *
636  * @param line          a reference to the line to indent.
637  * @param index         the current line index.
638  * @return              the new line index.
639  */
processSwitchBlock(string & line,size_t index)640 size_t ASEnhancer::processSwitchBlock(string& line, size_t index)
641 {
642 	size_t i = index;
643 	bool isPotentialKeyword = isCharPotentialHeader(line, i);
644 
645 	if (line[i] == '{')
646 	{
647 		sw.switchBraceCount++;
648 		if (lookingForCaseBrace)                      // if 1st after case statement
649 		{
650 			sw.unindentCase = true;                     // unindenting this case
651 			sw.unindentDepth++;
652 			lookingForCaseBrace = false;              // not looking now
653 		}
654 		return i;
655 	}
656 	lookingForCaseBrace = false;                      // no opening brace, don't indent
657 
658 	if (line[i] == '}')
659 	{
660 		sw.switchBraceCount--;
661 		if (sw.switchBraceCount == 0)                 // if end of switch statement
662 		{
663 			int lineUnindent = sw.unindentDepth;
664 			if (line.find_first_not_of(" \t") == i
665 			        && !switchStack.empty())
666 				lineUnindent = switchStack[switchStack.size() - 1].unindentDepth;
667 			if (shouldUnindentLine)
668 			{
669 				if (lineUnindent > 0)
670 					i -= unindentLine(line, lineUnindent);
671 				shouldUnindentLine = false;
672 			}
673 			switchDepth--;
674 			sw = switchStack.back();
675 			switchStack.pop_back();
676 		}
677 		return i;
678 	}
679 
680 	if (isPotentialKeyword
681 	        && (findKeyword(line, i, ASResource::AS_CASE)
682 	            || findKeyword(line, i, ASResource::AS_DEFAULT)))
683 	{
684 		if (sw.unindentCase)					// if unindented last case
685 		{
686 			sw.unindentCase = false;			// stop unindenting previous case
687 			sw.unindentDepth--;
688 		}
689 
690 		i = findCaseColon(line, i);
691 
692 		i++;
693 		for (; i < line.length(); i++)			// bypass whitespace
694 		{
695 			if (!isWhiteSpace(line[i]))
696 				break;
697 		}
698 		if (i < line.length())
699 		{
700 			if (line[i] == '{')
701 			{
702 				braceCount++;
703 				sw.switchBraceCount++;
704 				if (!isOneLineBlockReached(line, i))
705 					unindentNextLine = true;
706 				return i;
707 			}
708 		}
709 		lookingForCaseBrace = true;
710 		i--;									// need to process this char
711 		return i;
712 	}
713 	if (isPotentialKeyword)
714 	{
715 		string name = getCurrentWord(line, i);          // bypass the entire name
716 		i += name.length() - 1;
717 	}
718 	return i;
719 }
720 
721 /**
722  * unindent a line by a given number of tabsets
723  *    by erasing the leading whitespace from the line argument.
724  *
725  * @param line          a reference to the line to unindent.
726  * @param unindent      the number of tabsets to erase.
727  * @return              the number of characters erased.
728  */
unindentLine(string & line,int unindent) const729 int ASEnhancer::unindentLine(string& line, int unindent) const
730 {
731 	size_t whitespace = line.find_first_not_of(" \t");
732 
733 	if (whitespace == string::npos)         // if line is blank
734 		whitespace = line.length();         // must remove padding, if any
735 
736 	if (whitespace == 0)
737 		return 0;
738 
739 	size_t charsToErase = 0;
740 
741 	if (forceTab && indentLength != tabLength)
742 	{
743 		// replace tab indents with spaces
744 		convertForceTabIndentToSpaces(line);
745 		// remove the space indents
746 		size_t spaceIndentLength = line.find_first_not_of(" \t");
747 		charsToErase = unindent * indentLength;
748 		if (charsToErase <= spaceIndentLength)
749 			line.erase(0, charsToErase);
750 		else
751 			charsToErase = 0;
752 		// replace leading spaces with tab indents
753 		convertSpaceIndentToForceTab(line);
754 	}
755 	else if (useTabs)
756 	{
757 		charsToErase = unindent;
758 		if (charsToErase <= whitespace)
759 			line.erase(0, charsToErase);
760 		else
761 			charsToErase = 0;
762 	}
763 	else // spaces
764 	{
765 		charsToErase = unindent * indentLength;
766 		if (charsToErase <= whitespace)
767 			line.erase(0, charsToErase);
768 		else
769 			charsToErase = 0;
770 	}
771 
772 	return charsToErase;
773 }
774 
775 }   // end namespace astyle
776