// Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qqmldomformatdirectivescanner_p.h" #include "qqmldomoutwriter_p.h" #include "qqmldomlinewriter_p.h" #include "qqmldomindentinglinewriter_p.h" #include "qqmldomitem_p.h" #include "qqmldomcomments_p.h" #include QT_BEGIN_NAMESPACE namespace QQmlJS { namespace Dom { using DisabledRegionIt = OutWriter::OffsetToDisabledRegionMap::const_iterator; static inline OutWriter::RegionToCommentMap extractComments(const DomItem &it) { OutWriter::RegionToCommentMap comments; if (const RegionComments *cRegionsPtr = it.field(Fields::comments).as()) { comments = cRegionsPtr->regionComments(); } return comments; } /* \internal \brief Utility function to determine if two source locations overlap */ static inline bool overlaps(const SourceLocation &a, const SourceLocation &b) { return a.isValid() && b.isValid() && (a.begin() < b.end() && b.begin() < a.end()); } /* \internal \brief Utility function to determine the indent after skipping to format a piece of code */ static inline int indentAfterPartialFormatting(int initialIndent, QStringView code, LineWriterOptions options) { FormatTextStatus initialState = FormatTextStatus::initialStatus(initialIndent); FormatPartialStatus partialStatus({}, options.formatOptions, initialState); IndentingLineWriter indentingLineWriter([](QStringView line) { Q_UNUSED(line) }, QString(), options, partialStatus.currentStatus); OutWriter indentTracker(indentingLineWriter); const auto commentLines = code.split(u'\n'); for (const auto &line : commentLines) { if (!line.isEmpty()) { partialStatus = formatCodeLine(line, options.formatOptions, partialStatus.currentStatus); indentTracker.write(line); } } return indentTracker.indent; } /* \internal \brief Utility function to determine if a given location overlapping disabled region, returns an iterator to the region if found, or end() if not found */ static inline DisabledRegionIt findOverlappingRegion(const SourceLocation &loc, const OutWriter::OffsetToDisabledRegionMap &formatDisabledRegions) { if (!loc.isValid()) return formatDisabledRegions.cend(); return std::find_if(formatDisabledRegions.cbegin(), formatDisabledRegions.cend(), [&loc](const auto &it) { return it.isValid() && overlaps(loc, it); }); } QStringView OutWriter::attachedDisableCode(quint32 offset) const { if (formatDisabledRegions.contains(offset)) { const auto &loc = formatDisabledRegions.value(offset); return code.mid(loc.offset, loc.length); } return {}; } // This function examines the provided SourceLocation to determine if it overlaps with any regions // where formatting is disabled. If such a region is found and the formatter is currently enabled, // it writes the disabled region and disables the formatter. If no overlapping region is found, // the formatter is enabled. void OutWriter::maybeWriteDisabledRegion(const SourceLocation &loc) { if (!loc.isValid()) return; if (formatDisabledRegions.isEmpty()) return; if (const auto foundRegionIt = findOverlappingRegion(loc, formatDisabledRegions); foundRegionIt != formatDisabledRegions.end()) { if (isFormatterEnabled) { writeDisabledRegion(loc); isFormatterEnabled = false; } } else { isFormatterEnabled = true; } } // Decides whether the given region should be formatted or not, based on the // disabled regions found in the file and updates and returns the formatting enabled state. bool OutWriter::shouldFormat(const FileLocations::Tree &fLoc, FileLocationRegion region) { if (!fLoc || formatDisabledRegions.isEmpty()) return isFormatterEnabled; if (const auto regions = fLoc->info().regions; regions.contains(region)) { isFormatterEnabled = findOverlappingRegion(regions.value(region), formatDisabledRegions) == formatDisabledRegions.end(); } return isFormatterEnabled; } void OutWriter::scanFormatDirectives(QStringView code, const QList &comments) { // Disabled regions cannot be effective if the line writer options // are set to normalize or sort imports. const auto shouldScanDirectives = lineWriter.options().attributesSequence != LineWriterOptions::AttributesSequence::Normalize && !lineWriter.options().sortImports; if (!shouldScanDirectives) return; formatDisabledRegions = QmlFormat::identifyDisabledRegions(code, comments); } bool OutWriter::formatterEnabled() const { return isFormatterEnabled; } void OutWriter::writeDisabledRegion(const SourceLocation &loc) { const auto disabledCode = attachedDisableCode(loc.offset); int newIndent = indentAfterPartialFormatting(indent, disabledCode, lineWriter.options()); lineWriter.ensureNewline(); lineWriter.setLineIndent(0); indentNextlines = false; lineWriter.write(disabledCode); lineWriter.setLineIndent(newIndent); indentNextlines = true; } void OutWriter::maybeWriteComment(const Comment &comment) { maybeWriteDisabledRegion(comment.sourceLocation()); if (!skipComments && formatterEnabled()) { comment.write(*this); } // if disabled, maybe reenabled with this comment if (!formatterEnabled()) { auto directive = QmlFormat::directiveFromComment(comment.rawComment()); if (directive == QmlFormat::Directive::On) isFormatterEnabled = true; } } void OutWriter::itemStart(const DomItem &it) { if (skipComments) return; pendingComments.push(extractComments(it)); writePreComment(MainRegion); } void OutWriter::itemEnd() { if (skipComments) return; Q_ASSERT(!pendingComments.isEmpty()); writePostComment(MainRegion); pendingComments.pop(); } void OutWriter::writePreComment(FileLocationRegion region) { if (skipComments) return; const auto &comments = pendingComments.top(); if (comments.contains(region)) { const auto attachedComments = comments[region]; for (const auto &comment : attachedComments.preComments()) maybeWriteComment(comment); } } void OutWriter::writePostComment(FileLocationRegion region) { if (skipComments) return; auto &comments = pendingComments.top(); if (comments.contains(region)) { const auto attachedComments = comments[region]; for (const auto &comment : attachedComments.postComments()) maybeWriteComment(comment); comments.remove(region); } } static bool regionIncreasesIndentation(FileLocationRegion region) { switch (region) { case LeftBraceRegion: return true; case LeftBracketRegion: return true; default: return false; } Q_UNREACHABLE_RETURN(false); } static bool regionDecreasesIndentation(FileLocationRegion region) { switch (region) { case RightBraceRegion: return true; case RightBracketRegion: return true; default: return false; } Q_UNREACHABLE_RETURN(false); } /*! \internal Helper method for writeRegion(FileLocationRegion region) that allows to use \c{writeRegion(ColonTokenRegion);} instead of having to write out the more error-prone \c{writeRegion(ColonTokenRegion, ":");} for tokens and keywords. */ OutWriter &OutWriter::writeRegion(const FileLocations::Tree &fLoc, FileLocationRegion region) { using namespace Qt::Literals::StringLiterals; QString codeForRegion; switch (region) { case ComponentKeywordRegion: codeForRegion = u"component"_s; break; case IdColonTokenRegion: case ColonTokenRegion: codeForRegion = u":"_s; break; case ImportTokenRegion: codeForRegion = u"import"_s; break; case AsTokenRegion: codeForRegion = u"as"_s; break; case OnTokenRegion: codeForRegion = u"on"_s; break; case IdTokenRegion: codeForRegion = u"id"_s; break; case LeftBraceRegion: codeForRegion = u"{"_s; break; case RightBraceRegion: codeForRegion = u"}"_s; break; case LeftBracketRegion: codeForRegion = u"["_s; break; case RightBracketRegion: codeForRegion = u"]"_s; break; case LeftParenthesisRegion: codeForRegion = u"("_s; break; case RightParenthesisRegion: codeForRegion = u")"_s; break; case EnumKeywordRegion: codeForRegion = u"enum"_s; break; case DefaultKeywordRegion: codeForRegion = u"default"_s; break; case RequiredKeywordRegion: codeForRegion = u"required"_s; break; case ReadonlyKeywordRegion: codeForRegion = u"readonly"_s; break; case PropertyKeywordRegion: codeForRegion = u"property"_s; break; case FunctionKeywordRegion: codeForRegion = u"function"_s; break; case SignalKeywordRegion: codeForRegion = u"signal"_s; break; case ReturnKeywordRegion: codeForRegion = u"return"_s; break; case EllipsisTokenRegion: codeForRegion = u"..."_s; break; case EqualTokenRegion: codeForRegion = u"="_s; break; case PragmaKeywordRegion: codeForRegion = u"pragma"_s; break; case CommaTokenRegion: codeForRegion = u","_s; break; case ForKeywordRegion: codeForRegion = u"for"_s; break; case ElseKeywordRegion: codeForRegion = u"else"_s; break; case DoKeywordRegion: codeForRegion = u"do"_s; break; case WhileKeywordRegion: codeForRegion = u"while"_s; break; case TryKeywordRegion: codeForRegion = u"try"_s; break; case CatchKeywordRegion: codeForRegion = u"catch"_s; break; case FinallyKeywordRegion: codeForRegion = u"finally"_s; break; case CaseKeywordRegion: codeForRegion = u"case"_s; break; case ThrowKeywordRegion: codeForRegion = u"throw"_s; break; case ContinueKeywordRegion: codeForRegion = u"continue"_s; break; case BreakKeywordRegion: codeForRegion = u"break"_s; break; case QuestionMarkTokenRegion: codeForRegion = u"?"_s; break; case SemicolonTokenRegion: codeForRegion = u";"_s; break; case IfKeywordRegion: codeForRegion = u"if"_s; break; case SwitchKeywordRegion: codeForRegion = u"switch"_s; break; case YieldKeywordRegion: codeForRegion = u"yield"_s; break; case NewKeywordRegion: codeForRegion = u"new"_s; break; case ThisKeywordRegion: codeForRegion = u"this"_s; break; case SuperKeywordRegion: codeForRegion = u"super"_s; break; case StarTokenRegion: codeForRegion = u"*"_s; break; case DollarLeftBraceTokenRegion: codeForRegion = u"${"_s; break; case LeftBacktickTokenRegion: case RightBacktickTokenRegion: codeForRegion = u"`"_s; break; case FinalKeywordRegion: codeForRegion = u"final"_s; break; // not keywords: case ImportUriRegion: case IdNameRegion: case IdentifierRegion: case PragmaValuesRegion: case MainRegion: case OnTargetRegion: case TypeIdentifierRegion: case TypeModifierRegion: case FirstSemicolonTokenRegion: case SecondSemicolonRegion: case InOfTokenRegion: case OperatorTokenRegion: case VersionRegion: case EnumValueRegion: Q_ASSERT_X(false, "regionToString", "Using regionToString on a value or an identifier!"); return *this; } return writeRegion(fLoc, region, codeForRegion); } OutWriter &OutWriter::writeRegion(const FileLocations::Tree &fLoc, FileLocationRegion region, QStringView toWrite) { writePreComment(region); if (regionDecreasesIndentation(region)) decreaseIndent(1); if (shouldFormat(fLoc, region)) lineWriter.write(toWrite); if (regionIncreasesIndentation(region)) increaseIndent(1); writePostComment(region); return *this; } } // namespace Dom } // namespace QQmlJS QT_END_NAMESPACE