qmldom: line by line indentation/reformatting

Introduces support for line by line indentation and reformatting
This is done with a simplified restartable lexer and parser.

* qqmldomscanner: a tokenizer for a single line, built on the top of
the normal lexer. One of the key property is that the state can be
stored and restarted.
Thus after an edit, re-indenting or split of a line one can restart
the lexer with the state at the end of the previous line, and
re-evaluate from there on.
* qqmlcodeformatter: contains a stack based parser that is used for
syntax highlighting and indentation. The state is a stack of integers,
and along with it also the indent level is stored
and the accepted syntax is a superset of the grammar (typically
repetitions are allowed and order is more permissive).
This builds on the top of the scanner, and also allows restarts.
The public interface focuses on having the stored state of the parser
and lexer, and being able to update it after a line of text, or give
the correct indentation for a line.
* qqmldomindentinglinewriter: has a LineWriter that uses the
CodeFormatter internally to re-indent every line.

Change-Id: Ifce9ed14ecd157bec65fb740b2c7ee8a5fc0729a
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Fawzi Mohamed 2021-03-26 21:59:49 +01:00
parent 7d013de058
commit 6d36ff2cf5
11 changed files with 2612 additions and 0 deletions

View File

@ -73,6 +73,9 @@
%token T_GET "get"
%token T_SET "set"
-- token representing no token
%token T_NONE
%token T_ERROR
-- states for line by line parsing

View File

@ -17,6 +17,7 @@ qt_internal_add_module(QmlDomPrivate
qqmldomastcreator.cpp qqmldomastcreator_p.h
qqmldomastdumper.cpp qqmldomastdumper_p.h
qqmldomattachedinfo.cpp qqmldomattachedinfo_p.h
qqmldomcodeformatter.cpp qqmldomcodeformatter_p.h
qqmldomcomments.cpp qqmldomcomments_p.h
qqmldomcompare.cpp qqmldomcompare_p.h
qqmldomconstants.cpp qqmldomconstants_p.h
@ -26,6 +27,7 @@ qt_internal_add_module(QmlDomPrivate
qqmldomfieldfilter.cpp qqmldomfieldfilter_p.h
qqmldomfilewriter.cpp qqmldomfilewriter_p.h
qqmldomfunctionref_p.h
qqmldomindentinglinewriter.cpp qqmldomindentinglinewriter_p.h
qqmldomitem.cpp qqmldomitem_p.h
qqmldommock.cpp qqmldommock_p.h
qqmldomlinewriter.cpp qqmldomlinewriter_p.h
@ -34,6 +36,7 @@ qt_internal_add_module(QmlDomPrivate
qqmldompath.cpp qqmldompath_p.h
qqmldomstringdumper.cpp qqmldomstringdumper_p.h
qqmldomreformatter.cpp qqmldomreformatter_p.h
qqmldomscanner.cpp qqmldomscanner_p.h
qqmldomtop.cpp qqmldomtop_p.h
qqmldomtypesreader.cpp qqmldomtypesreader_p.h
DEFINES

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,275 @@
// Copyright (C) 2022 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
#ifndef QQMLDOMCODEFORMATTER_P_H
#define QQMLDOMCODEFORMATTER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qqmldom_global.h"
#include "qqmldomfunctionref_p.h"
#include "qqmldomscanner_p.h"
#include "qqmldomlinewriter_p.h"
#include <QtCore/QStack>
#include <QtCore/QList>
#include <QtCore/QSet>
#include <QtCore/QVector>
#include <QtCore/QMetaObject>
QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
class QMLDOM_EXPORT FormatTextStatus
{
Q_GADGET
public:
enum class StateType : quint8 {
Invalid = 0,
TopmostIntro, // The first line in a "topmost" definition.
TopQml, // root state for qml
TopJs, // root for js
ObjectdefinitionOrJs, // file starts with identifier
MultilineCommentStart,
MultilineCommentCont,
ImportStart, // after 'import'
ImportMaybeDotOrVersionOrAs, // after string or identifier
ImportDot, // after .
ImportMaybeAs, // after version
ImportAs,
PropertyStart, // after 'property'
PropertyModifiers, // after 'default' or readonly
RequiredProperty, // after required
PropertyListOpen, // after 'list' as a type
PropertyName, // after the type
PropertyMaybeInitializer, // after the identifier
ComponentStart, // after component
ComponentName, // after component Name
TypeAnnotation, // after a : starting a type annotation // new, to do
TypeParameter, // after a < in a type annotation (starting type parameters) // new, to do
EnumStart, // after 'enum'
SignalStart, // after 'signal'
SignalMaybeArglist, // after identifier
SignalArglistOpen, // after '('
FunctionStart, // after 'function'
FunctionArglistOpen, // after '(' starting function argument list
FunctionArglistClosed, // after ')' in argument list, expecting '{'
BindingOrObjectdefinition, // after an identifier
BindingAssignment, // after : in a binding
ObjectdefinitionOpen, // after {
Expression,
ExpressionContinuation, // at the end of the line, when the next line definitely is a
// continuation
ExpressionMaybeContinuation, // at the end of the line, when the next line may be an
// expression
ExpressionOrObjectdefinition, // after a binding starting with an identifier ("x: foo")
ExpressionOrLabel, // when expecting a statement and getting an identifier
ParenOpen, // opening ( in expression
BracketOpen, // opening [ in expression
ObjectliteralOpen, // opening { in expression
ObjectliteralAssignment, // after : in object literal
BracketElementStart, // after starting bracket_open or after ',' in bracket_open
BracketElementMaybeObjectdefinition, // after an identifier in bracket_element_start
TernaryOp, // The ? : operator
TernaryOpAfterColon, // after the : in a ternary
JsblockOpen,
EmptyStatement, // for a ';', will be popped directly
BreakcontinueStatement, // for continue/break, may be followed by identifier
IfStatement, // After 'if'
MaybeElse, // after the first substatement in an if
ElseClause, // The else line of an if-else construct.
ConditionOpen, // Start of a condition in 'if', 'while', entered after opening paren
Substatement, // The first line after a conditional or loop construct.
SubstatementOpen, // The brace that opens a substatement block.
LabelledStatement, // after a label
ReturnStatement, // After 'return'
ThrowStatement, // After 'throw'
StatementWithCondition, // After the 'for', 'while', ... token
StatementWithConditionParenOpen, // While inside the (...)
TryStatement, // after 'try'
CatchStatement, // after 'catch', nested in try_statement
FinallyStatement, // after 'finally', nested in try_statement
MaybeCatchOrFinally, // after ther closing '}' of try_statement and catch_statement,
// nested in try_statement
DoStatement, // after 'do'
DoStatementWhileParenOpen, // after '(' in while clause
SwitchStatement, // After 'switch' token
CaseStart, // after a 'case' or 'default' token
CaseCont // after the colon in a case/default
};
Q_ENUM(StateType)
static QString stateToString(StateType type);
class State
{
public:
quint16 savedIndentDepth = 0;
StateType type = StateType::Invalid;
bool operator==(const State &other) const
{
return type == other.type && savedIndentDepth == other.savedIndentDepth;
}
QString typeStr() const { return FormatTextStatus::stateToString(type); }
};
static bool isBracelessState(StateType type)
{
return type == StateType::IfStatement || type == StateType::ElseClause
|| type == StateType::Substatement || type == StateType::BindingAssignment
|| type == StateType::BindingOrObjectdefinition;
}
static bool isExpressionEndState(StateType type)
{
return type == StateType::TopmostIntro || type == StateType::TopJs
|| type == StateType::ObjectdefinitionOpen || type == StateType::DoStatement
|| type == StateType::JsblockOpen || type == StateType::SubstatementOpen
|| type == StateType::BracketOpen || type == StateType::ParenOpen
|| type == StateType::CaseCont || type == StateType::ObjectliteralOpen;
}
static FormatTextStatus initialStatus(int baseIndent = 0)
{
return FormatTextStatus {
Scanner::State {},
QVector<State>({ State { quint16(baseIndent), StateType::TopmostIntro } }), baseIndent
};
}
size_t size() const { return states.size(); }
State state(int belowTop = 0) const;
void pushState(StateType type, quint16 savedIndentDepth)
{
states.append(State { savedIndentDepth, type });
}
State popState()
{
if (states.isEmpty()) {
Q_ASSERT(false);
return State();
}
State res = states.last();
states.removeLast();
return res;
}
Scanner::State lexerState = {};
QVector<State> states;
int finalIndent = 0;
};
class QMLDOM_EXPORT FormatPartialStatus
{
Q_GADGET
public:
using OnEnterCallback =
function_ref<void(FormatTextStatus::StateType newState, int *indentDepth,
int *savedIndentDepth, const FormatPartialStatus &fStatus)>;
// to determine whether a line was joined, Tokenizer needs a
// newline character at the end, lease ensure that line contains it
FormatPartialStatus() = default;
FormatPartialStatus(const FormatPartialStatus &o) = default;
FormatPartialStatus &operator=(const FormatPartialStatus &o) = default;
FormatPartialStatus(QStringView line, const FormatOptions &options,
const FormatTextStatus &initialStatus)
: line(line),
options(options),
initialStatus(initialStatus),
currentStatus(initialStatus),
currentIndent(0),
tokenIndex(0)
{
Scanner::State startState = initialStatus.lexerState;
currentIndent = initialStatus.finalIndent;
Scanner tokenize;
lineTokens = tokenize(line, startState);
currentStatus.lexerState = tokenize.state();
}
void enterState(FormatTextStatus::StateType newState);
void leaveState(bool statementDone);
void turnIntoState(FormatTextStatus::StateType newState);
const Token &tokenAt(int idx) const;
int tokenCount() const { return lineTokens.size(); }
int column(int index) const;
QStringView tokenText(const Token &token) const;
void handleTokens();
bool tryInsideExpression(bool alsoExpression);
bool tryStatement();
void defaultOnEnter(FormatTextStatus::StateType newState, int *indentDepth,
int *savedIndentDepth) const;
int indentLine();
int indentForNewLineAfter() const;
void recalculateWithIndent(int indent);
void dump() const;
QStringView line;
FormatOptions options;
FormatTextStatus initialStatus;
FormatTextStatus currentStatus;
int indentOffset = 0;
int currentIndent = 0;
QList<Token> lineTokens;
int tokenIndex = 0;
};
QMLDOM_EXPORT int indentForLineStartingWithToken(const FormatTextStatus &oldStatus,
const FormatOptions &options,
int token = QQmlJSGrammar::T_ERROR);
QMLDOM_EXPORT FormatPartialStatus formatCodeLine(QStringView line, const FormatOptions &options,
const FormatTextStatus &initialStatus);
} // namespace Dom
} // namespace QQmlJs
QT_END_NAMESPACE
#endif // QQMLDOMCODEFORMATTER_P_H

View File

@ -0,0 +1,126 @@
// Copyright (C) 2022 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 "qqmldomindentinglinewriter_p.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QRegularExpression>
QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
FormatPartialStatus &IndentingLineWriter::fStatus()
{
if (!m_fStatusValid) {
m_fStatus = formatCodeLine(m_currentLine, m_options.formatOptions, m_preCachedStatus);
m_fStatusValid = true;
}
return m_fStatus;
}
void IndentingLineWriter::willCommit()
{
m_preCachedStatus = fStatus().currentStatus;
}
void IndentingLineWriter::reindentAndSplit(QString eol, bool eof)
{
bool shouldReindent = m_reindent;
indentAgain:
// maybe re-indent
if (shouldReindent && m_columnNr == 0) {
setLineIndent(fStatus().indentLine());
}
if (!eol.isEmpty() || eof) {
LineWriterOptions::TrailingSpace trailingSpace;
if (!m_currentLine.isEmpty() && m_currentLine.trimmed().isEmpty()) {
// space only line
const Scanner::State &oldState = m_preCachedStatus.lexerState;
if (oldState.isMultilineComment())
trailingSpace = m_options.commentTrailingSpace;
else if (oldState.isMultiline())
trailingSpace = m_options.stringTrailingSpace;
else
trailingSpace = m_options.codeTrailingSpace;
// in the LSP we will probably want to treat is specially if it is the line with the
// cursor, of if indentation of it is requested
} else {
const Scanner::State &currentState = fStatus().currentStatus.lexerState;
if (currentState.isMultilineComment()) {
trailingSpace = m_options.commentTrailingSpace;
} else if (currentState.isMultiline()) {
trailingSpace = m_options.stringTrailingSpace;
} else {
const int kind =
(fStatus().lineTokens.isEmpty() ? T_EOL
: fStatus().lineTokens.last().lexKind);
if (Token::lexKindIsComment(kind)) {
// a // comment...
trailingSpace = m_options.commentTrailingSpace;
Q_ASSERT(fStatus().currentStatus.state().type
!= FormatTextStatus::StateType::MultilineCommentCont
&& fStatus().currentStatus.state().type
!= FormatTextStatus::StateType::
MultilineCommentStart); // these should have been
// handled above
} else {
trailingSpace = m_options.codeTrailingSpace;
}
}
}
handleTrailingSpace(trailingSpace);
}
// maybe split long line
if (m_options.maxLineLength > 0 && m_currentLine.size() > m_options.maxLineLength) {
int possibleSplit = -1;
if (fStatus().lineTokens.size() > 1) {
// {}[] should already be handled (handle also here?)
int minLen = 0;
while (minLen < m_currentLine.size() && m_currentLine.at(minLen).isSpace())
++minLen;
minLen = column(minLen) + m_options.minContentLength;
int maxLen = qMax(minLen + m_options.strongMaxLineExtra, m_options.maxLineLength);
std::array<QSet<int>, 2> splitSequence(
{ QSet<int>({ // try split after ',','||','&&'
QQmlJSGrammar::T_COMMA, QQmlJSGrammar::T_AND_AND,
QQmlJSGrammar::T_OR_OR }),
QSet<int>({ // try split after '('
QQmlJSGrammar::T_LPAREN }) });
// try split after other binary operators?
int minSplit = m_currentLine.size();
for (const QSet<int> &splitOnToken : splitSequence) {
for (int iToken = 0; iToken < fStatus().tokenCount(); ++iToken) {
const Token t = fStatus().tokenAt(iToken);
int tCol = column(t.end());
if (splitOnToken.contains(t.lexKind) && tCol > minLen) {
if (tCol <= maxLen && possibleSplit < t.end())
possibleSplit = t.end();
if (t.end() < minSplit)
minSplit = t.end();
}
}
if (possibleSplit > 0)
break;
}
if (possibleSplit == -1 && minSplit + 4 < m_currentLine.size())
possibleSplit = minSplit;
if (possibleSplit > 0) {
lineChanged();
quint32 oChange = eolToWrite().size();
changeAtOffset(m_utf16Offset + possibleSplit, oChange, 0,
0); // line & col change updated in commitLine
commitLine(eolToWrite(), TextAddType::NewlineSplit, possibleSplit);
shouldReindent = true;
goto indentAgain;
}
}
}
// maybe write out
if (!eol.isEmpty() || eof)
commitLine(eol);
}
} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE

View File

@ -0,0 +1,62 @@
// Copyright (C) 2022 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
#ifndef QQMLDOMINDENTIGLINEWRITER_P
#define QQMLDOMINDENTIGLINEWRITER_P
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qqmldom_global.h"
#include "qqmldomcodeformatter_p.h"
#include "qqmldomlinewriter_p.h"
#include <QtQml/private/qqmljssourcelocation_p.h>
#include <QtCore/QAtomicInt>
#include <functional>
QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
QMLDOM_EXPORT class IndentingLineWriter : public LineWriter
{
Q_GADGET
public:
IndentingLineWriter(SinkF innerSink, QString fileName,
const LineWriterOptions &options = LineWriterOptions(),
const FormatTextStatus &initialStatus = FormatTextStatus::initialStatus(),
int lineNr = 0, int columnNr = 0, int utf16Offset = 0,
QString currentLine = QString())
: LineWriter(innerSink, fileName, options, lineNr, columnNr, utf16Offset, currentLine),
m_preCachedStatus(initialStatus)
{
}
void reindentAndSplit(QString eol, bool eof = false) override;
FormatPartialStatus &fStatus();
void lineChanged() override { m_fStatusValid = false; }
void willCommit() override;
bool reindent() const { return m_reindent; }
void setReindent(bool v) { m_reindent = v; }
private:
Q_DISABLE_COPY_MOVE(IndentingLineWriter)
protected:
FormatTextStatus m_preCachedStatus;
bool m_fStatusValid = false;
FormatPartialStatus m_fStatus;
};
} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE
#endif

View File

@ -0,0 +1,456 @@
// Copyright (C) 2022 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 "qqmldomscanner_p.h"
#include "qqmldomerrormessage_p.h"
#include <QtCore/QMetaEnum>
#include <algorithm>
QT_BEGIN_NAMESPACE
using namespace QQmlJS::Dom;
static void addLexToken(QList<Token> &tokens, int tokenKind, QQmlJS::Lexer &lexer,
bool &regexpMayFollow)
{
switch (tokenKind) {
case QQmlJSGrammar::T_DIVIDE_:
case QQmlJSGrammar::T_DIVIDE_EQ:
if (regexpMayFollow) {
QQmlJS::Lexer::RegExpBodyPrefix prefix;
if (tokenKind == QQmlJSGrammar::T_DIVIDE_)
prefix = QQmlJS::Lexer::NoPrefix;
else
prefix = QQmlJS::Lexer::EqualPrefix;
if (lexer.scanRegExp(prefix)) {
regexpMayFollow = false;
break;
} else {
qCWarning(domLog) << "lexing error scannign regexp in" << lexer.code()
<< lexer.errorCode() << lexer.errorMessage();
}
break;
} else if (tokenKind == QQmlJSGrammar::T_DIVIDE_) {
regexpMayFollow = true;
}
Q_FALLTHROUGH();
case QQmlJSGrammar::T_AND:
case QQmlJSGrammar::T_AND_AND:
case QQmlJSGrammar::T_AND_EQ:
case QQmlJSGrammar::T_ARROW:
case QQmlJSGrammar::T_EQ:
case QQmlJSGrammar::T_EQ_EQ:
case QQmlJSGrammar::T_EQ_EQ_EQ:
case QQmlJSGrammar::T_GE:
case QQmlJSGrammar::T_GT:
case QQmlJSGrammar::T_GT_GT:
case QQmlJSGrammar::T_GT_GT_EQ:
case QQmlJSGrammar::T_GT_GT_GT:
case QQmlJSGrammar::T_GT_GT_GT_EQ:
case QQmlJSGrammar::T_LE:
case QQmlJSGrammar::T_LT:
case QQmlJSGrammar::T_LT_LT:
case QQmlJSGrammar::T_LT_LT_EQ:
case QQmlJSGrammar::T_MINUS:
case QQmlJSGrammar::T_MINUS_EQ:
case QQmlJSGrammar::T_MINUS_MINUS:
case QQmlJSGrammar::T_NOT:
case QQmlJSGrammar::T_NOT_EQ:
case QQmlJSGrammar::T_NOT_EQ_EQ:
case QQmlJSGrammar::T_OR:
case QQmlJSGrammar::T_OR_EQ:
case QQmlJSGrammar::T_OR_OR:
case QQmlJSGrammar::T_PLUS:
case QQmlJSGrammar::T_PLUS_EQ:
case QQmlJSGrammar::T_PLUS_PLUS:
case QQmlJSGrammar::T_QUESTION:
case QQmlJSGrammar::T_QUESTION_DOT:
case QQmlJSGrammar::T_QUESTION_QUESTION:
case QQmlJSGrammar::T_REMAINDER:
case QQmlJSGrammar::T_REMAINDER_EQ:
case QQmlJSGrammar::T_STAR:
case QQmlJSGrammar::T_STAR_EQ:
case QQmlJSGrammar::T_STAR_STAR:
case QQmlJSGrammar::T_STAR_STAR_EQ:
case QQmlJSGrammar::T_TILDE:
case QQmlJSGrammar::T_XOR:
case QQmlJSGrammar::T_XOR_EQ:
case QQmlJSGrammar::T_AT:
case QQmlJSGrammar::T_AUTOMATIC_SEMICOLON:
case QQmlJSGrammar::T_COMPATIBILITY_SEMICOLON:
case QQmlJSGrammar::T_SEMICOLON:
case QQmlJSGrammar::T_COLON:
case QQmlJSGrammar::T_COMMA:
case QQmlJSGrammar::T_LBRACE:
case QQmlJSGrammar::T_LBRACKET:
case QQmlJSGrammar::T_LPAREN:
case QQmlJSGrammar::T_ELLIPSIS:
regexpMayFollow = true;
break;
case QQmlJSGrammar::T_FUNCTION:
// might contain a space at the end...
tokens.append(Token(lexer.tokenStartColumn() - 1,
lexer.tokenLength() - ((lexer.tokenText().endsWith(u' ')) ? 1 : 0),
tokenKind));
return;
case QQmlJSGrammar::T_DOT:
case QQmlJSGrammar::T_RBRACE:
case QQmlJSGrammar::T_RBRACKET:
case QQmlJSGrammar::T_RPAREN:
regexpMayFollow = false;
break;
// template used to expand to a string plus a delimiter for the ${ and }, now
// we use a + as delimiter
case QQmlJSGrammar::T_TEMPLATE_HEAD:
regexpMayFollow = true;
tokens.append(Token(lexer.tokenStartColumn() - 1, lexer.tokenLength() - 2, tokenKind));
tokens.append(Token(lexer.tokenStartColumn() + lexer.tokenLength() - 3, 2,
QQmlJSGrammar::T_PLUS));
return;
case QQmlJSGrammar::T_TEMPLATE_MIDDLE:
regexpMayFollow = true;
tokens.append(Token(lexer.tokenStartColumn() - 1, 1, QQmlJSGrammar::T_PLUS));
tokens.append(Token(lexer.tokenStartColumn(), lexer.tokenLength() - 3, tokenKind));
tokens.append(Token(lexer.tokenStartColumn() + lexer.tokenLength() - 3, 2,
QQmlJSGrammar::T_PLUS));
return;
case QQmlJSGrammar::T_TEMPLATE_TAIL:
regexpMayFollow = true;
tokens.append(Token(lexer.tokenStartColumn() - 1, 1, QQmlJSGrammar::T_PLUS));
tokens.append(Token(lexer.tokenStartColumn(), lexer.tokenLength() - 1, tokenKind));
return;
case T_PARTIAL_TEMPLATE_MIDDLE:
regexpMayFollow = true;
tokens.append(Token(lexer.tokenStartColumn() - 1, 1, QQmlJSGrammar::T_PLUS));
tokens.append(Token(lexer.tokenStartColumn(), lexer.tokenLength() - 1, tokenKind));
return;
case QQmlJSGrammar::T_MULTILINE_STRING_LITERAL:
case QQmlJSGrammar::T_NO_SUBSTITUTION_TEMPLATE:
case QQmlJSGrammar::T_STRING_LITERAL:
case T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_TEMPLATE_HEAD:
regexpMayFollow = (tokenKind == QQmlJSGrammar::T_TEMPLATE_MIDDLE
|| tokenKind == QQmlJSGrammar::T_TEMPLATE_HEAD);
break;
case QQmlJSGrammar::T_VERSION_NUMBER:
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (lexer.state().currentChar == u'.') {
int offset = lexer.tokenStartColumn() - 1;
int length = lexer.tokenLength();
tokenKind = lexer.lex();
Q_ASSERT(tokenKind == QQmlJSGrammar::T_DOT);
tokenKind = lexer.lex();
Q_ASSERT(tokenKind == QQmlJSGrammar::T_VERSION_NUMBER);
length += 1 + lexer.tokenLength();
tokens.append(Token(offset, length, QQmlJSGrammar::T_NUMERIC_LITERAL));
return;
}
#endif
break;
default:
break;
}
// avoid newline (on multiline comments/strings)
qsizetype len = lexer.code().length();
if (lexer.code().endsWith(u'\n'))
--len;
len -= lexer.tokenStartColumn() - 1;
if (len < 0)
len = 0;
if (lexer.tokenLength() < len)
len = lexer.tokenLength();
tokens.append(Token(lexer.tokenStartColumn() - 1, len, tokenKind));
}
bool Token::lexKindIsDelimiter(int kind)
{
switch (kind) {
case QQmlJSGrammar::T_AND:
case QQmlJSGrammar::T_AND_AND:
case QQmlJSGrammar::T_AND_EQ:
case QQmlJSGrammar::T_ARROW:
case QQmlJSGrammar::T_EQ:
case QQmlJSGrammar::T_EQ_EQ:
case QQmlJSGrammar::T_EQ_EQ_EQ:
case QQmlJSGrammar::T_GE:
case QQmlJSGrammar::T_GT:
case QQmlJSGrammar::T_GT_GT:
case QQmlJSGrammar::T_GT_GT_EQ:
case QQmlJSGrammar::T_GT_GT_GT:
case QQmlJSGrammar::T_GT_GT_GT_EQ:
case QQmlJSGrammar::T_LE:
case QQmlJSGrammar::T_LT:
case QQmlJSGrammar::T_LT_LT:
case QQmlJSGrammar::T_LT_LT_EQ:
case QQmlJSGrammar::T_MINUS:
case QQmlJSGrammar::T_MINUS_EQ:
case QQmlJSGrammar::T_MINUS_MINUS:
case QQmlJSGrammar::T_NOT:
case QQmlJSGrammar::T_NOT_EQ:
case QQmlJSGrammar::T_NOT_EQ_EQ:
case QQmlJSGrammar::T_OR:
case QQmlJSGrammar::T_OR_EQ:
case QQmlJSGrammar::T_OR_OR:
case QQmlJSGrammar::T_PLUS:
case QQmlJSGrammar::T_PLUS_EQ:
case QQmlJSGrammar::T_PLUS_PLUS:
case QQmlJSGrammar::T_QUESTION:
case QQmlJSGrammar::T_QUESTION_DOT:
case QQmlJSGrammar::T_QUESTION_QUESTION:
case QQmlJSGrammar::T_REMAINDER:
case QQmlJSGrammar::T_REMAINDER_EQ:
case QQmlJSGrammar::T_STAR:
case QQmlJSGrammar::T_STAR_EQ:
case QQmlJSGrammar::T_STAR_STAR:
case QQmlJSGrammar::T_STAR_STAR_EQ:
case QQmlJSGrammar::T_TILDE:
case QQmlJSGrammar::T_XOR:
case QQmlJSGrammar::T_XOR_EQ:
case QQmlJSGrammar::T_AT:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsQmlReserved(int kind)
{
switch (kind) {
case QQmlJSGrammar::T_AS:
case QQmlJSGrammar::T_IMPORT:
case QQmlJSGrammar::T_SIGNAL:
case QQmlJSGrammar::T_PROPERTY:
case QQmlJSGrammar::T_READONLY:
case QQmlJSGrammar::T_COMPONENT:
case QQmlJSGrammar::T_REQUIRED:
case QQmlJSGrammar::T_ON:
case QQmlJSGrammar::T_ENUM:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsComment(int kind)
{
switch (kind) {
case QQmlJSGrammar::T_COMMENT:
case T_PARTIAL_COMMENT:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsJSKeyword(int kind)
{
switch (kind) {
case QQmlJSGrammar::T_BREAK:
case QQmlJSGrammar::T_CASE:
case QQmlJSGrammar::T_CATCH:
case QQmlJSGrammar::T_CLASS:
case QQmlJSGrammar::T_CONST:
case QQmlJSGrammar::T_CONTINUE:
case QQmlJSGrammar::T_DEBUGGER:
case QQmlJSGrammar::T_DEFAULT:
case QQmlJSGrammar::T_DELETE:
case QQmlJSGrammar::T_DO:
case QQmlJSGrammar::T_ELSE:
case QQmlJSGrammar::T_ENUM:
case QQmlJSGrammar::T_EXPORT:
case QQmlJSGrammar::T_EXTENDS:
case QQmlJSGrammar::T_FALSE:
case QQmlJSGrammar::T_FINALLY:
case QQmlJSGrammar::T_FOR:
case QQmlJSGrammar::T_FROM:
case QQmlJSGrammar::T_GET:
case QQmlJSGrammar::T_IF:
case QQmlJSGrammar::T_IN:
case QQmlJSGrammar::T_INSTANCEOF:
case QQmlJSGrammar::T_LET:
case QQmlJSGrammar::T_NEW:
case QQmlJSGrammar::T_RETURN:
case QQmlJSGrammar::T_SUPER:
case QQmlJSGrammar::T_SWITCH:
case QQmlJSGrammar::T_THEN:
case QQmlJSGrammar::T_THIS:
case QQmlJSGrammar::T_THROW:
case QQmlJSGrammar::T_VOID:
case QQmlJSGrammar::T_WHILE:
case QQmlJSGrammar::T_WITH:
case QQmlJSGrammar::T_YIELD:
case QQmlJSGrammar::T_VAR:
case QQmlJSGrammar::T_FUNCTION_STAR:
case QQmlJSGrammar::T_FUNCTION:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsIdentifier(int kind)
{
switch (kind) {
case QQmlJSGrammar::T_IDENTIFIER:
case QQmlJSGrammar::T_COMPONENT:
case QQmlJSGrammar::T_REQUIRED:
case QQmlJSGrammar::T_AS:
case QQmlJSGrammar::T_PRAGMA:
case QQmlJSGrammar::T_IMPORT:
case QQmlJSGrammar::T_RESERVED_WORD:
case QQmlJSGrammar::T_SET:
case QQmlJSGrammar::T_SIGNAL:
case QQmlJSGrammar::T_PROPERTY:
case QQmlJSGrammar::T_PUBLIC:
case QQmlJSGrammar::T_READONLY:
case QQmlJSGrammar::T_NULL:
case QQmlJSGrammar::T_OF:
case QQmlJSGrammar::T_ON:
case QQmlJSGrammar::T_STATIC:
case QQmlJSGrammar::T_TRUE:
case QQmlJSGrammar::T_TRY:
case QQmlJSGrammar::T_TYPEOF:
case QQmlJSGrammar::T_WITHOUTAS:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsStringType(int kind)
{
switch (kind) {
case T_PARTIAL_TEMPLATE_MIDDLE:
case QQmlJSGrammar::T_MULTILINE_STRING_LITERAL:
case QQmlJSGrammar::T_NO_SUBSTITUTION_TEMPLATE:
case QQmlJSGrammar::T_STRING_LITERAL:
case T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_TEMPLATE_HEAD:
return true;
default:
break;
}
return false;
}
bool Token::lexKindIsInvalid(int kind)
{
switch (kind) {
case T_NONE:
case T_EOL:
case QQmlJSGrammar::EOF_SYMBOL:
case QQmlJSGrammar::T_ERROR:
case QQmlJSGrammar::T_FEED_JS_EXPRESSION:
case QQmlJSGrammar::T_FEED_JS_MODULE:
case QQmlJSGrammar::T_FEED_JS_SCRIPT:
case QQmlJSGrammar::T_FEED_JS_STATEMENT:
case QQmlJSGrammar::T_FEED_UI_OBJECT_MEMBER:
case QQmlJSGrammar::T_FEED_UI_PROGRAM:
case QQmlJSGrammar::REDUCE_HERE:
case QQmlJSGrammar::T_FORCE_BLOCK:
case QQmlJSGrammar::T_FORCE_DECLARATION:
case QQmlJSGrammar::T_FOR_LOOKAHEAD_OK:
return true;
default:
break;
}
return false;
}
void Token::dump(Sink s, QStringView line) const
{
s(u"{");
sinkInt(s, offset);
s(u", ");
sinkInt(s, length);
s(u", Token::");
s(QString::number(lexKind));
s(u"}");
QStringView value = line.mid(offset, length);
if (!value.isEmpty()) {
s(u":");
sinkEscaped(s, value);
}
}
QList<Token> Scanner::operator()(QStringView text, const Scanner::State &startState)
{
_state = startState;
QList<Token> tokens;
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
Q_ASSERT_X(false, "qmldomscanner",
"line by line (progressive lexing supported only when building against Qt6.5 or "
"higher)");
#else
{
QQmlJS::Lexer lexer(nullptr, QQmlJS::Lexer::LexMode::LineByLine);
lexer.setState(startState.state);
QString line = text.toString();
if (!(line.endsWith(u"\n") || line.endsWith(u"\r")))
line += u'\n';
lexer.setCode(line, -1, _qmlMode, QQmlJS::Lexer::CodeContinuation::Continue);
while (true) {
int tokenKind = lexer.lex();
if (tokenKind == T_EOL || tokenKind == QQmlJSGrammar::EOF_SYMBOL)
break;
addLexToken(tokens, tokenKind, lexer, _state.regexpMightFollow);
}
_state.state = lexer.state();
}
#endif
return tokens;
}
Scanner::State Scanner::state() const
{
return _state;
}
bool Scanner::State::isMultiline() const
{
switch (state.tokenKind) {
case T_PARTIAL_COMMENT:
case T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL:
case T_PARTIAL_TEMPLATE_HEAD:
case T_PARTIAL_TEMPLATE_MIDDLE:
return true;
default:
break;
}
return false;
}
bool Scanner::State::isMultilineComment() const
{
switch (state.tokenKind) {
case T_PARTIAL_COMMENT:
return true;
default:
break;
}
return false;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,131 @@
// Copyright (C) 2022 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
#ifndef QQMLDOMSCANNER_P_H
#define QQMLDOMSCANNER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qqmldom_global.h"
#include "qqmldomstringdumper_p.h"
#include <QStringList>
#include <QStringView>
#include <QtQml/private/qqmljslexer_p.h>
#include <QtQml/private/qqmljsgrammar_p.h>
QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
enum {
T_EOL = 2000,
T_PARTIAL_COMMENT = 2001,
T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL = 2002,
T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL = 2003,
T_PARTIAL_TEMPLATE_HEAD = 2004,
T_PARTIAL_TEMPLATE_MIDDLE = 2005,
T_NONE = 2006
};
#else
constexpr auto T_NONE = QQmlJSGrammar::T_NONE;
constexpr auto T_EOL = QQmlJSGrammar::T_EOL;
constexpr auto T_PARTIAL_COMMENT = QQmlJSGrammar::T_PARTIAL_COMMENT;
constexpr auto T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL =
QQmlJSGrammar::T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL;
constexpr auto T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL =
QQmlJSGrammar::T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL;
constexpr auto T_PARTIAL_TEMPLATE_HEAD = QQmlJSGrammar::T_PARTIAL_TEMPLATE_HEAD;
constexpr auto T_PARTIAL_TEMPLATE_MIDDLE = QQmlJSGrammar::T_PARTIAL_TEMPLATE_MIDDLE;
#endif
class QMLDOM_EXPORT Token
{
Q_GADGET
public:
static bool lexKindIsDelimiter(int kind);
static bool lexKindIsJSKeyword(int kind);
static bool lexKindIsIdentifier(int kind);
static bool lexKindIsStringType(int kind);
static bool lexKindIsInvalid(int kind);
static bool lexKindIsQmlReserved(int kind);
static bool lexKindIsComment(int kind);
inline Token() = default;
inline Token(int o, int l, int lexKind) : offset(o), length(l), lexKind(lexKind) { }
inline int begin() const { return offset; }
inline int end() const { return offset + length; }
void dump(Sink s, QStringView line = QStringView()) const;
QString toString(QStringView line = QStringView()) const
{
return dumperToString([line, this](Sink s) { this->dump(s, line); });
}
static int compare(const Token &t1, const Token &t2)
{
if (int c = t1.offset - t2.offset)
return c;
if (int c = t1.length - t2.length)
return c;
return int(t1.lexKind) - int(t2.lexKind);
}
int offset = 0;
int length = 0;
int lexKind = T_NONE;
};
inline int operator==(const Token &t1, const Token &t2)
{
return Token::compare(t1, t2) == 0;
}
inline int operator!=(const Token &t1, const Token &t2)
{
return Token::compare(t1, t2) != 0;
}
class QMLDOM_EXPORT Scanner
{
public:
struct QMLDOM_EXPORT State
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
Lexer::State state {};
#else
struct LState
{
int tokenKind = {};
friend QDebug operator<<(QDebug dbg, const LState &s)
{
return dbg << "tokenKind:" << s.tokenKind;
}
} state;
#endif
bool regexpMightFollow = true;
bool isMultiline() const;
bool isMultilineComment() const;
};
QList<Token> operator()(QStringView text, const State &startState);
State state() const;
private:
bool _qmlMode = true;
State _state;
};
} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE
#endif

View File

@ -44,6 +44,7 @@ add_library(qmldomlib
../qqmldomastcreator.cpp ../qqmldomastcreator_p.h
../qqmldomastdumper.cpp ../qqmldomastdumper_p.h
../qqmldomattachedinfo.cpp ../qqmldomattachedinfo_p.h
../qqmldomcodeformatter.cpp ../qqmldomcodeformatter_p.h
../qqmldomcomments.cpp ../qqmldomcomments_p.h
../qqmldomcompare.cpp ../qqmldomcompare_p.h
../qqmldomconstants_p.h
@ -60,6 +61,7 @@ add_library(qmldomlib
../qqmldomoutwriter.cpp ../qqmldomoutwriter_p.h
../qqmldompath.cpp ../qqmldompath_p.h
../qqmldomreformatter.cpp ../qqmldomreformatter_p.h
../qqmldomscanner.cpp ../qqmldomscanner_p.h
../qqmldomstringdumper.cpp ../qqmldomstringdumper_p.h
../qqmldomtop.cpp ../qqmldomtop_p.h
../qqmldomtypesreader.cpp ../qqmldomtypesreader_p.h

View File

@ -8,6 +8,9 @@ width: 640
height: 480
title: qsTr("Scroll")
property var arr: [1,2,3]
property var arrTrailingComma: [1,2,3,]
Rectangle {
anchors.fill: parent

View File

@ -4,6 +4,7 @@
#ifndef TST_QMLDOMCODEFORMATTER_H
#define TST_QMLDOMCODEFORMATTER_H
#include <QtQmlDom/private/qqmldomlinewriter_p.h>
#include <QtQmlDom/private/qqmldomindentinglinewriter_p.h>
#include <QtQmlDom/private/qqmldomoutwriter_p.h>
#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>
@ -24,6 +25,177 @@ class TestReformatter : public QObject
Q_OBJECT
public:
private slots:
void reindent_data()
{
QTest::addColumn<QString>("inFile");
QTest::addColumn<QString>("outFile");
QTest::newRow("file1") << QStringLiteral(u"file1.qml") << QStringLiteral(u"file1.qml");
QTest::newRow("file1 unindented")
<< QStringLiteral(u"file1Unindented.qml") << QStringLiteral(u"file1.qml");
}
void reindent()
{
QFETCH(QString, inFile);
QFETCH(QString, outFile);
QFile fIn(QLatin1String(QT_QMLTEST_DATADIR) + QLatin1String("/reformatter/") + inFile);
if (!fIn.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "could not open file" << inFile;
return;
}
QFile fOut(QLatin1String(QT_QMLTEST_DATADIR) + QLatin1String("/reformatter/") + outFile);
if (!fOut.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "could not open file" << outFile;
return;
}
QTextStream in(&fIn);
QTextStream out(&fOut);
QString resultStr;
QTextStream res(&resultStr);
QString line = in.readLine();
IndentingLineWriter lw([&res](QStringView s) { res << s; }, QLatin1String("*testStream*"));
QList<SourceLocation *> sourceLocations;
while (!line.isNull()) {
SourceLocation *loc = new SourceLocation;
sourceLocations.append(loc);
lw.write(line, loc);
lw.write(u"\n");
line = in.readLine();
}
lw.eof();
res.flush();
QString fullRes = resultStr;
res.seek(0);
line = out.readLine();
QString resLine = res.readLine();
int iLoc = 0;
int nextLoc = 0;
while (!line.isNull() && !resLine.isNull()) {
QCOMPARE(resLine, line);
if (iLoc == nextLoc && iLoc < sourceLocations.size()) {
QString l2 =
fullRes.mid(sourceLocations[iLoc]->offset, sourceLocations[iLoc]->length);
if (!l2.contains(QLatin1Char('\n'))) {
QCOMPARE(l2, line);
} else {
qDebug() << "skip checks of multiline location (line was split)" << l2;
iLoc -= l2.count(QLatin1Char('\n'));
}
++nextLoc;
} else {
qDebug() << "skip multiline recover";
}
++iLoc;
line = out.readLine();
resLine = res.readLine();
}
QCOMPARE(resLine.isNull(), line.isNull());
for (auto sLoc : sourceLocations)
delete sLoc;
}
void lineByLineReformatter_data()
{
QTest::addColumn<QString>("inFile");
QTest::addColumn<QString>("outFile");
QTest::addColumn<LineWriterOptions>("options");
LineWriterOptions defaultOptions;
LineWriterOptions noReorderOptions;
noReorderOptions.attributesSequence = LineWriterOptions::AttributesSequence::Preserve;
QTest::newRow("file1") << QStringLiteral(u"file1.qml")
<< QStringLiteral(u"file1Reformatted.qml") << defaultOptions;
QTest::newRow("file2") << QStringLiteral(u"file2.qml")
<< QStringLiteral(u"file2Reformatted.qml") << defaultOptions;
QTest::newRow("commentedFile")
<< QStringLiteral(u"commentedFile.qml")
<< QStringLiteral(u"commentedFileReformatted.qml") << defaultOptions;
QTest::newRow("required") << QStringLiteral(u"required.qml")
<< QStringLiteral(u"requiredReformatted.qml") << defaultOptions;
QTest::newRow("inline") << QStringLiteral(u"inline.qml")
<< QStringLiteral(u"inlineReformatted.qml") << defaultOptions;
QTest::newRow("spread") << QStringLiteral(u"spread.qml")
<< QStringLiteral(u"spreadReformatted.qml") << defaultOptions;
QTest::newRow("template") << QStringLiteral(u"template.qml")
<< QStringLiteral(u"templateReformatted.qml") << defaultOptions;
QTest::newRow("file1NoReorder")
<< QStringLiteral(u"file1.qml") << QStringLiteral(u"file1Reformatted2.qml")
<< noReorderOptions;
}
void lineByLineReformatter()
{
QFETCH(QString, inFile);
QFETCH(QString, outFile);
QFETCH(LineWriterOptions, options);
QString baseDir = QLatin1String(QT_QMLTEST_DATADIR) + QLatin1String("/reformatter");
QStringList qmltypeDirs =
QStringList({ baseDir, QLibraryInfo::path(QLibraryInfo::Qml2ImportsPath) });
DomItem env = DomEnvironment::create(
qmltypeDirs,
QQmlJS::Dom::DomEnvironment::Option::SingleThreaded
| QQmlJS::Dom::DomEnvironment::Option::NoDependencies);
QString testFilePath = baseDir + QLatin1Char('/') + inFile;
DomItem tFile;
env.loadBuiltins();
env.loadFile(
testFilePath, QString(),
[&tFile](Path, const DomItem &, const DomItem &newIt) { tFile = newIt; },
LoadOption::DefaultLoad);
env.loadPendingDependencies();
MutableDomItem myFile = tFile.field(Fields::currentItem);
QString resultStr;
QTextStream res(&resultStr);
IndentingLineWriter lw([&res](QStringView s) { res << s; }, QLatin1String("*testStream*"),
options);
OutWriter ow(lw);
DomItem qmlFile = tFile.field(Fields::currentItem);
qmlFile.writeOut(ow);
lw.eof();
res.flush();
QString fullRes = resultStr;
res.seek(0);
QFile fOut(baseDir + QLatin1Char('/') + outFile);
if (!fOut.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "could not open file" << outFile;
return;
}
QTextStream out(&fOut);
QString line = out.readLine();
QString resLine = res.readLine();
int iLine = 0;
auto writeReformatted = [fullRes]() {
qDebug().noquote().nospace() << "Reformatted output:\n"
<< "-----------------\n"
<< fullRes << "-----------------\n";
};
while (!line.isNull() && !resLine.isNull()) {
++iLine;
if (resLine != line)
writeReformatted();
QCOMPARE(resLine, line);
line = out.readLine();
resLine = res.readLine();
}
if (resLine.isNull() != line.isNull()) {
writeReformatted();
qDebug() << "reformatted at end" << resLine.isNull() << resLine
<< "reference at end:" << line.isNull() << line;
}
QCOMPARE(resLine.isNull(), line.isNull());
}
void manualReformatter_data()
{