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.h"
11
12//-----------------------------------------------------------------------------
13// astyle namespace
14//-----------------------------------------------------------------------------
15
16namespace 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 */
27void 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 */
87void 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 */
153void 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 */
174void 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 */
191size_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 */
239int 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 */
279bool 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 */
328bool 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 */
377bool 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 */
449void 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 */
640size_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 */
729int 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