2022-05-13 13:12:05 +00:00
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
2019-12-13 15:10:46 +00:00
# include <QCoreApplication>
# include <QFile>
2020-06-18 08:25:30 +00:00
# include <QTextStream>
2019-12-13 15:10:46 +00:00
# include <QtQml/private/qqmljslexer_p.h>
# include <QtQml/private/qqmljsparser_p.h>
# include <QtQml/private/qqmljsengine_p.h>
# include <QtQml/private/qqmljsastvisitor_p.h>
# include <QtQml/private/qqmljsast_p.h>
2021-04-19 14:43:21 +00:00
# include <QtQmlDom/private/qqmldomitem_p.h>
# include <QtQmlDom/private/qqmldomexternalitems_p.h>
# include <QtQmlDom/private/qqmldomtop_p.h>
# include <QtQmlDom/private/qqmldomoutwriter_p.h>
2019-12-13 15:10:46 +00:00
# if QT_CONFIG(commandlineparser)
2021-04-19 14:43:21 +00:00
# include <QCommandLineParser>
2019-12-13 15:10:46 +00:00
# endif
2023-01-24 09:45:21 +00:00
# include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
2021-10-25 16:30:26 +00:00
2021-04-19 14:43:21 +00:00
using namespace QQmlJS : : Dom ;
2019-12-13 15:10:46 +00:00
2021-02-07 22:07:04 +00:00
struct Options
{
bool verbose = false ;
bool inplace = false ;
bool force = false ;
bool tabs = false ;
bool valid = false ;
2021-04-19 14:43:21 +00:00
bool normalize = false ;
2021-10-25 16:30:26 +00:00
bool ignoreSettings = false ;
bool writeDefaultSettings = false ;
2022-11-06 20:40:50 +00:00
bool objectsSpacing = false ;
2022-11-23 18:33:40 +00:00
bool functionsSpacing = false ;
2021-02-07 22:07:04 +00:00
int indentWidth = 4 ;
bool indentWidthSet = false ;
QString newline = " native " ;
QStringList files ;
QStringList arguments ;
QStringList errors ;
} ;
2023-12-05 09:38:43 +00:00
// TODO refactor
// Move out to the LineWriterOptions class / helper
static LineWriterOptions composeLwOptions ( const Options & options , QStringView code )
2019-12-13 15:10:46 +00:00
{
2021-04-19 14:43:21 +00:00
LineWriterOptions lwOptions ;
lwOptions . formatOptions . indentSize = options . indentWidth ;
lwOptions . formatOptions . useTabs = options . tabs ;
lwOptions . updateOptions = LineWriterOptions : : Update : : None ;
if ( options . newline = = " native " ) {
// find out current line endings...
int newlineIndex = code . indexOf ( QChar ( u ' \n ' ) ) ;
int crIndex = code . indexOf ( QChar ( u ' \r ' ) ) ;
if ( newlineIndex > = 0 ) {
if ( crIndex > = 0 ) {
if ( crIndex + 1 = = newlineIndex )
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : Windows ;
else
qWarning ( ) . noquote ( ) < < " Invalid line ending in file, using default " ;
} else {
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : Unix ;
2020-01-17 13:28:09 +00:00
}
2021-04-19 14:43:21 +00:00
} else if ( crIndex > = 0 ) {
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : OldMacOs ;
} else {
qWarning ( ) . noquote ( ) < < " Unknown line ending in file, using default " ;
2020-01-17 13:28:09 +00:00
}
2021-04-19 14:43:21 +00:00
} else if ( options . newline = = " macos " ) {
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : OldMacOs ;
} else if ( options . newline = = " windows " ) {
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : Windows ;
} else if ( options . newline = = " unix " ) {
lwOptions . lineEndings = LineWriterOptions : : LineEndings : : Unix ;
} else {
2023-12-05 09:38:43 +00:00
qWarning ( ) . noquote ( ) < < " Unknown line ending type " < < options . newline < < " , using default " ;
2020-01-17 13:28:09 +00:00
}
2021-04-19 14:43:21 +00:00
if ( options . normalize )
lwOptions . attributesSequence = LineWriterOptions : : AttributesSequence : : Normalize ;
else
lwOptions . attributesSequence = LineWriterOptions : : AttributesSequence : : Preserve ;
2022-11-06 20:40:50 +00:00
lwOptions . objectsSpacing = options . objectsSpacing ;
2022-11-23 18:33:40 +00:00
lwOptions . functionsSpacing = options . functionsSpacing ;
2023-12-05 09:38:43 +00:00
return lwOptions ;
}
static void logParsingErrors ( const DomItem & fileItem , const QString & filename )
{
fileItem . iterateErrors (
[ ] ( const DomItem & , const ErrorMessage & msg ) {
errorToQDebug ( msg ) ;
return true ;
} ,
true ) ;
qWarning ( ) . noquote ( ) < < " Failed to parse " < < filename ;
}
// TODO
// refactor this workaround. ExternalOWningItem is not recognized as an owning type
// in ownerAs.
static std : : shared_ptr < ExternalOwningItem > getFileItemOwner ( const DomItem & fileItem )
{
std : : shared_ptr < ExternalOwningItem > filePtr = nullptr ;
switch ( fileItem . internalKind ( ) ) {
case DomType : : JsFile :
filePtr = fileItem . ownerAs < JsFile > ( ) ;
break ;
default :
filePtr = fileItem . ownerAs < QmlFile > ( ) ;
break ;
}
return filePtr ;
}
// TODO refactor
// Introduce better encapsulation and separation of concerns and move to DOM API
// returns a DomItem corresponding to the loaded file and bool indicating the validity of the file
static std : : pair < DomItem , bool > parse ( const QString & filename )
{
auto envPtr =
DomEnvironment : : create ( QStringList ( ) ,
QQmlJS : : Dom : : DomEnvironment : : Option : : SingleThreaded
| QQmlJS : : Dom : : DomEnvironment : : Option : : NoDependencies ) ;
// placeholder for a node
// containing metadata (ExternalItemInfo) about the loaded file
DomItem fMetadataItem ;
envPtr - > loadFile (
FileToLoad : : fromFileSystem ( envPtr , filename ) ,
// callback called when everything is loaded that receives the
// loaded external file pair (path, oldValue, newValue)
[ & fMetadataItem ] ( Path , const DomItem & , const DomItem & extItemInfo ) {
fMetadataItem = extItemInfo ;
} ,
LoadOption : : DefaultLoad ) ;
auto fItem = fMetadataItem . fileObject ( ) ;
auto filePtr = getFileItemOwner ( fItem ) ;
return { fItem , filePtr & & filePtr - > isValid ( ) } ;
}
static bool parseFile ( const QString & filename , const Options & options )
{
const auto [ fileItem , validFile ] = parse ( filename ) ;
if ( ! validFile ) {
logParsingErrors ( fileItem , filename ) ;
return false ;
}
// Turn AST back into source code
if ( options . verbose )
qWarning ( ) . noquote ( ) < < " Dumping " < < filename ;
2024-01-29 12:50:32 +00:00
const auto & code = getFileItemOwner ( fileItem ) - > code ( ) ;
2023-12-05 09:38:43 +00:00
auto lwOptions = composeLwOptions ( options , code ) ;
WriteOutChecks checks = WriteOutCheck : : Default ;
//Disable writeOutChecks for some usecases
if ( options . force | |
code . size ( ) > 32000 | |
fileItem . internalKind ( ) = = DomType : : JsFile ) {
checks = WriteOutCheck : : None ;
}
2022-11-06 20:40:50 +00:00
QQmlJS::Dom::OutWriter. Refactoring
The refactoring consists of:
- Changing writeOut & writeOutForFile API
to return boolean instead of MutableDomItem, which better reflects
the existing usecases improving consistency of the data model*
Moreover, previous API was exposing DomItem, which was not "committed
to base" (MutableDomItem.commitToBase()), meaning it was exposing the
"unmerged" Item alongside with the "temporary environment"
- Refactoring & renaming OutWriter::updatedFile
breaking it into smaller chunks preserving
only necessary functionality
- Adding some comments / documentation
Before this commit, the writeOut API was "exposing",so called,
"updatedFile", which is basically the copy of the original fileItem +
renewed scriptExpressions which were modified during the writeOut of
the original fileItem.
The idea behind the "mutating" Dom API is that one has to create a
MutableDomItem, do some changes to it and then "commit" them.
This process is also facilitated by the creation of separate Env.
(git analogy might be handy here:
We create a separate branch, where all the mutation will happen and then
we "merge" this branch)
However, in the writeOutForFile usecase this "updatedFile" was needed
only for the verifying of the consistency of the "writtenOut" DOM,
however the API was exposing it further back to the caller sites,
without "committing".
The potential issue here is inconsistency of the data Model.
On one side we have an original File Item owned by the Base Env,
on the other side we have an "updatedFile" which is owned by another Env.
Taking into account that there are no usecases requiring "exposing"
"updatedFile", but also no need for "committing" the changes,
It's arguably better to keep that temporary "updatedFile" locally,
not exposing it outside the writeOutForFile function. Thereby improving
consistency of the data model.
Change-Id: If45eca4b4d6d703e2a76d0580f124d0292af5ed8
Reviewed-by: Semih Yavuz <semih.yavuz@qt.io>
2023-12-05 09:42:23 +00:00
bool res = false ;
2021-04-19 14:43:21 +00:00
if ( options . inplace ) {
if ( options . verbose )
qWarning ( ) . noquote ( ) < < " Writing to file " < < filename ;
FileWriter fw ;
2022-10-19 10:50:31 +00:00
const unsigned numberOfBackupFiles = 0 ;
2023-12-05 09:38:43 +00:00
res = fileItem . writeOut ( filename , numberOfBackupFiles , lwOptions , & fw , checks ) ;
2021-04-19 14:43:21 +00:00
} else {
QFile out ;
out . open ( stdout , QIODevice : : WriteOnly ) ;
LineWriter lw ( [ & out ] ( QStringView s ) { out . write ( s . toUtf8 ( ) ) ; } , filename , lwOptions ) ;
OutWriter ow ( lw ) ;
2023-12-05 09:38:43 +00:00
res = fileItem . writeOutForFile ( ow , checks ) ;
2021-04-19 14:43:21 +00:00
ow . flush ( ) ;
}
QQmlJS::Dom::OutWriter. Refactoring
The refactoring consists of:
- Changing writeOut & writeOutForFile API
to return boolean instead of MutableDomItem, which better reflects
the existing usecases improving consistency of the data model*
Moreover, previous API was exposing DomItem, which was not "committed
to base" (MutableDomItem.commitToBase()), meaning it was exposing the
"unmerged" Item alongside with the "temporary environment"
- Refactoring & renaming OutWriter::updatedFile
breaking it into smaller chunks preserving
only necessary functionality
- Adding some comments / documentation
Before this commit, the writeOut API was "exposing",so called,
"updatedFile", which is basically the copy of the original fileItem +
renewed scriptExpressions which were modified during the writeOut of
the original fileItem.
The idea behind the "mutating" Dom API is that one has to create a
MutableDomItem, do some changes to it and then "commit" them.
This process is also facilitated by the creation of separate Env.
(git analogy might be handy here:
We create a separate branch, where all the mutation will happen and then
we "merge" this branch)
However, in the writeOutForFile usecase this "updatedFile" was needed
only for the verifying of the consistency of the "writtenOut" DOM,
however the API was exposing it further back to the caller sites,
without "committing".
The potential issue here is inconsistency of the data Model.
On one side we have an original File Item owned by the Base Env,
on the other side we have an "updatedFile" which is owned by another Env.
Taking into account that there are no usecases requiring "exposing"
"updatedFile", but also no need for "committing" the changes,
It's arguably better to keep that temporary "updatedFile" locally,
not exposing it outside the writeOutForFile function. Thereby improving
consistency of the data model.
Change-Id: If45eca4b4d6d703e2a76d0580f124d0292af5ed8
Reviewed-by: Semih Yavuz <semih.yavuz@qt.io>
2023-12-05 09:42:23 +00:00
return res ;
2019-12-13 15:10:46 +00:00
}
2021-02-07 22:07:04 +00:00
Options buildCommandLineOptions ( const QCoreApplication & app )
2019-12-13 15:10:46 +00:00
{
# if QT_CONFIG(commandlineparser)
QCommandLineParser parser ;
parser . setApplicationDescription ( " Formats QML files according to the QML Coding Conventions. " ) ;
parser . addHelpOption ( ) ;
parser . addVersionOption ( ) ;
2021-04-19 14:43:21 +00:00
parser . addOption (
QCommandLineOption ( { " V " , " verbose " } ,
QStringLiteral ( " Verbose mode. Outputs more detailed information. " ) ) ) ;
2019-12-13 15:10:46 +00:00
2021-10-25 16:30:26 +00:00
QCommandLineOption writeDefaultsOption (
QStringList ( ) < < " write-defaults " ,
QLatin1String ( " Writes defaults settings to .qmlformat.ini and exits (Warning: This "
" will overwrite any existing settings and comments!) " ) ) ;
parser . addOption ( writeDefaultsOption ) ;
QCommandLineOption ignoreSettings ( QStringList ( ) < < " ignore-settings " ,
QLatin1String ( " Ignores all settings files and only takes "
" command line options into consideration " ) ) ;
parser . addOption ( ignoreSettings ) ;
2021-04-19 14:43:21 +00:00
parser . addOption ( QCommandLineOption (
{ " i " , " inplace " } ,
QStringLiteral ( " Edit file in-place instead of outputting to stdout. " ) ) ) ;
2019-12-13 15:10:46 +00:00
2021-04-19 14:43:21 +00:00
parser . addOption ( QCommandLineOption ( { " f " , " force " } ,
QStringLiteral ( " Continue even if an error has occurred. " ) ) ) ;
2020-01-15 10:25:18 +00:00
2020-09-02 10:34:29 +00:00
parser . addOption (
QCommandLineOption ( { " t " , " tabs " } , QStringLiteral ( " Use tabs instead of spaces. " ) ) ) ;
parser . addOption ( QCommandLineOption ( { " w " , " indent-width " } ,
QStringLiteral ( " How many spaces are used when indenting. " ) ,
" width " , " 4 " ) ) ;
2021-04-19 14:43:21 +00:00
parser . addOption ( QCommandLineOption ( { " n " , " normalize " } ,
QStringLiteral ( " Reorders the attributes of the objects "
" according to the QML Coding Guidelines. " ) ) ) ;
2020-06-18 08:25:30 +00:00
parser . addOption ( QCommandLineOption (
{ " F " , " files " } , QStringLiteral ( " Format all files listed in file, in-place " ) , " file " ) ) ;
2021-04-19 14:43:21 +00:00
parser . addOption ( QCommandLineOption (
{ " l " , " newline " } ,
QStringLiteral ( " Override the new line format to use (native macos unix windows). " ) ,
" newline " , " native " ) ) ;
2020-03-18 08:19:31 +00:00
2022-11-06 20:40:50 +00:00
parser . addOption ( QCommandLineOption ( QStringList ( ) < < " objects-spacing " , QStringLiteral ( " Ensure spaces between objects (only works with normalize option). " ) ) ) ;
2022-11-23 18:33:40 +00:00
parser . addOption ( QCommandLineOption ( QStringList ( ) < < " functions-spacing " , QStringLiteral ( " Ensure spaces between functions (only works with normalize option). " ) ) ) ;
2019-12-13 15:10:46 +00:00
parser . addPositionalArgument ( " filenames " , " files to be processed by qmlformat " ) ;
parser . process ( app ) ;
2021-10-25 16:30:26 +00:00
if ( parser . isSet ( writeDefaultsOption ) ) {
Options options ;
options . writeDefaultSettings = true ;
options . valid = true ;
return options ;
}
2021-02-07 22:07:04 +00:00
bool indentWidthOkay = false ;
const int indentWidth = parser . value ( " indent-width " ) . toInt ( & indentWidthOkay ) ;
if ( ! indentWidthOkay ) {
Options options ;
options . errors . push_back ( " Error: Invalid value passed to -w " ) ;
return options ;
}
2019-12-13 15:10:46 +00:00
2021-02-07 22:07:04 +00:00
QStringList files ;
2021-04-19 14:43:21 +00:00
if ( ! parser . value ( " files " ) . isEmpty ( ) ) {
2021-04-13 11:13:14 +00:00
QFile file ( parser . value ( " files " ) ) ;
file . open ( QIODevice : : Text | QIODevice : : ReadOnly ) ;
if ( file . isOpen ( ) ) {
QTextStream in ( & file ) ;
while ( ! in . atEnd ( ) ) {
QString file = in . readLine ( ) ;
if ( file . isEmpty ( ) )
continue ;
files . push_back ( file ) ;
}
2021-02-07 22:07:04 +00:00
}
2020-09-02 10:34:29 +00:00
}
2021-02-07 22:07:04 +00:00
Options options ;
options . verbose = parser . isSet ( " verbose " ) ;
options . inplace = parser . isSet ( " inplace " ) ;
options . force = parser . isSet ( " force " ) ;
options . tabs = parser . isSet ( " tabs " ) ;
2021-04-19 14:43:21 +00:00
options . normalize = parser . isSet ( " normalize " ) ;
2021-10-25 16:30:26 +00:00
options . ignoreSettings = parser . isSet ( " ignore-settings " ) ;
2022-11-06 20:40:50 +00:00
options . objectsSpacing = parser . isSet ( " objects-spacing " ) ;
2022-11-23 18:33:40 +00:00
options . functionsSpacing = parser . isSet ( " functions-spacing " ) ;
2021-02-07 22:07:04 +00:00
options . valid = true ;
options . indentWidth = indentWidth ;
options . indentWidthSet = parser . isSet ( " indent-width " ) ;
options . newline = parser . value ( " newline " ) ;
options . files = files ;
options . arguments = parser . positionalArguments ( ) ;
return options ;
# else
return Options { } ;
# endif
}
int main ( int argc , char * argv [ ] )
{
QCoreApplication app ( argc , argv ) ;
QCoreApplication : : setApplicationName ( " qmlformat " ) ;
2021-06-22 08:19:46 +00:00
QCoreApplication : : setApplicationVersion ( QT_VERSION_STR ) ;
2021-02-07 22:07:04 +00:00
2021-10-25 16:30:26 +00:00
QQmlToolingSettings settings ( QLatin1String ( " qmlformat " ) ) ;
const QString & useTabsSetting = QStringLiteral ( " UseTabs " ) ;
settings . addOption ( useTabsSetting ) ;
const QString & indentWidthSetting = QStringLiteral ( " IndentWidth " ) ;
settings . addOption ( indentWidthSetting , 4 ) ;
const QString & normalizeSetting = QStringLiteral ( " NormalizeOrder " ) ;
settings . addOption ( normalizeSetting ) ;
const QString & newlineSetting = QStringLiteral ( " NewlineType " ) ;
settings . addOption ( newlineSetting , QStringLiteral ( " native " ) ) ;
2022-11-06 20:40:50 +00:00
const QString & objectsSpacingSetting = QStringLiteral ( " ObjectsSpacing " ) ;
settings . addOption ( objectsSpacingSetting ) ;
2022-11-23 18:33:40 +00:00
const QString & functionsSpacingSetting = QStringLiteral ( " FunctionsSpacing " ) ;
settings . addOption ( functionsSpacingSetting ) ;
2021-02-07 22:07:04 +00:00
const auto options = buildCommandLineOptions ( app ) ;
if ( ! options . valid ) {
for ( const auto & error : options . errors ) {
qWarning ( ) . noquote ( ) < < error ;
}
2020-09-02 10:34:29 +00:00
return - 1 ;
}
2021-10-25 16:30:26 +00:00
if ( options . writeDefaultSettings )
return settings . writeDefaults ( ) ? 0 : - 1 ;
auto getSettings = [ & ] ( const QString & file , Options options ) {
2023-10-13 11:30:27 +00:00
// Perform formatting inplace if --files option is set.
if ( ! options . files . isEmpty ( ) )
options . inplace = true ;
2021-10-25 16:30:26 +00:00
if ( options . ignoreSettings | | ! settings . search ( file ) )
return options ;
Options perFileOptions = options ;
// Allow for tab settings to be overwritten by the command line
if ( ! options . indentWidthSet ) {
if ( settings . isSet ( indentWidthSetting ) )
perFileOptions . indentWidth = settings . value ( indentWidthSetting ) . toInt ( ) ;
if ( settings . isSet ( useTabsSetting ) )
perFileOptions . tabs = settings . value ( useTabsSetting ) . toBool ( ) ;
}
if ( settings . isSet ( normalizeSetting ) )
perFileOptions . normalize = settings . value ( normalizeSetting ) . toBool ( ) ;
if ( settings . isSet ( newlineSetting ) )
perFileOptions . newline = settings . value ( newlineSetting ) . toString ( ) ;
2022-11-06 20:40:50 +00:00
if ( settings . isSet ( objectsSpacingSetting ) )
perFileOptions . objectsSpacing = settings . value ( objectsSpacingSetting ) . toBool ( ) ;
2022-11-23 18:33:40 +00:00
if ( settings . isSet ( functionsSpacingSetting ) )
perFileOptions . functionsSpacing = settings . value ( functionsSpacingSetting ) . toBool ( ) ;
2021-10-25 16:30:26 +00:00
return perFileOptions ;
} ;
2021-02-07 22:07:04 +00:00
bool success = true ;
if ( ! options . files . isEmpty ( ) ) {
if ( ! options . arguments . isEmpty ( ) )
2020-06-18 08:25:30 +00:00
qWarning ( ) < < " Warning: Positional arguments are ignored when -F is used " ;
2021-02-07 22:07:04 +00:00
for ( const QString & file : options . files ) {
Q_ASSERT ( ! file . isEmpty ( ) ) ;
2020-06-18 08:25:30 +00:00
2021-10-25 16:30:26 +00:00
if ( ! parseFile ( file , getSettings ( file , options ) ) )
2020-06-18 08:25:30 +00:00
success = false ;
}
} else {
2021-02-07 22:07:04 +00:00
for ( const QString & file : options . arguments ) {
2021-10-25 16:30:26 +00:00
if ( ! parseFile ( file , getSettings ( file , options ) ) )
2020-06-18 08:25:30 +00:00
success = false ;
}
2019-12-13 15:10:46 +00:00
}
return success ? 0 : 1 ;
}