From f40b3c22f4b101c4d0ae70718db71b9623caedfa Mon Sep 17 00:00:00 2001 From: Christian Kamm <christian.d.kamm@nokia.com> Date: Thu, 25 Nov 2010 13:38:15 +0100 Subject: [PATCH] QmlJS: Add JSLint-style warnings for common JS traps. Task-number: QTCREATORBUG-3071 Reviewed-by: Erik Verbruggen --- src/libs/qmljs/qmljscheck.cpp | 350 ++++++++++++++++++++++++++++++++++ src/libs/qmljs/qmljscheck.h | 38 ++++ 2 files changed, 388 insertions(+) diff --git a/src/libs/qmljs/qmljscheck.cpp b/src/libs/qmljs/qmljscheck.cpp index 9ac19d8ed08..de741da5379 100644 --- a/src/libs/qmljs/qmljscheck.cpp +++ b/src/libs/qmljs/qmljscheck.cpp @@ -188,6 +188,151 @@ public: ExpressionNode *_ast; }; +class FunctionBodyCheck : protected Visitor +{ +public: + QList<DiagnosticMessage> operator()(FunctionExpression *function, Check::Options options) + { + _options = options; + _messages.clear(); + _declaredFunctions.clear(); + _declaredVariables.clear(); + _possiblyUndeclaredUses.clear(); + _seenNonDeclarationStatement = false; + + _formalParameterNames.clear(); + for (FormalParameterList *plist = function->formals; plist; plist = plist->next) { + if (plist->name) + _formalParameterNames += plist->name->asString(); + } + + Node::accept(function->body, this); + return _messages; + } + +protected: + void postVisit(Node *ast) + { + if (!_seenNonDeclarationStatement && ast->statementCast() + && !cast<VariableStatement *>(ast)) { + _seenNonDeclarationStatement = true; + } + } + + bool visit(IdentifierExpression *ast) + { + if (!ast->name) + return false; + const QString name = ast->name->asString(); + if (!_declaredFunctions.contains(name) && !_declaredVariables.contains(name)) + _possiblyUndeclaredUses[name].append(ast->identifierToken); + return false; + } + + bool visit(VariableStatement *ast) + { + if (_options & Check::WarnDeclarationsNotStartOfFunction && _seenNonDeclarationStatement) { + warning(ast->declarationKindToken, Check::tr("declarations should be at the start of a function")); + } + return true; + } + + bool visit(VariableDeclaration *ast) + { + if (!ast->name) + return true; + const QString name = ast->name->asString(); + + if (_formalParameterNames.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("already a formal parameter")); + return true; + } + if (_declaredFunctions.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("already declared as function")); + return true; + } + if (_declaredVariables.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("duplicate declaration")); + return true; + } + + if (_possiblyUndeclaredUses.contains(name)) { + if (_options & Check::WarnUseBeforeDeclaration) { + foreach (const SourceLocation &loc, _possiblyUndeclaredUses.value(name)) { + warning(loc, Check::tr("variable is used before being declared")); + } + } + _possiblyUndeclaredUses.remove(name); + } + _declaredVariables[name] = ast; + + return true; + } + + bool visit(FunctionDeclaration *ast) + { + if (_options & Check::WarnDeclarationsNotStartOfFunction &&_seenNonDeclarationStatement) { + warning(ast->functionToken, Check::tr("declarations should be at the start of a function")); + } + + return visit(static_cast<FunctionExpression *>(ast)); + } + + bool visit(FunctionExpression *ast) + { + if (!ast->name) + return false; + const QString name = ast->name->asString(); + + if (_formalParameterNames.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("already a formal parameter")); + return false; + } + if (_declaredVariables.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("already declared as var")); + return false; + } + if (_declaredFunctions.contains(name)) { + if (_options & Check::WarnDuplicateDeclaration) + warning(ast->identifierToken, Check::tr("duplicate declaration")); + return false; + } + + if (FunctionDeclaration *decl = cast<FunctionDeclaration *>(ast)) { + if (_possiblyUndeclaredUses.contains(name)) { + if (_options & Check::WarnUseBeforeDeclaration) { + foreach (const SourceLocation &loc, _possiblyUndeclaredUses.value(name)) { + warning(loc, Check::tr("function is used before being declared")); + } + } + _possiblyUndeclaredUses.remove(name); + } + _declaredFunctions[name] = decl; + } + + return false; + } + +private: + void warning(const SourceLocation &loc, const QString &message) + { + _messages.append(DiagnosticMessage(DiagnosticMessage::Warning, loc, message)); + } + + Check::Options _options; + QList<DiagnosticMessage> _messages; + QStringList _formalParameterNames; + QHash<QString, VariableDeclaration *> _declaredVariables; + QHash<QString, FunctionDeclaration *> _declaredFunctions; + QHash<QString, QList<SourceLocation> > _possiblyUndeclaredUses; + bool _seenNonDeclarationStatement; +}; + } // end of anonymous namespace @@ -197,6 +342,10 @@ Check::Check(Document::Ptr doc, const Snapshot &snapshot, const Context *linkedC , _context(*linkedContextNoScope) , _scopeBuilder(&_context, doc, snapshot) , _ignoreTypeErrors(false) + , _options(WarnDangerousNonStrictEqualityChecks | WarnBlocks | WarnWith + | WarnVoid | WarnCommaExpression | WarnExpressionStatement + | WarnAssignInCondition | WarnUseBeforeDeclaration | WarnDuplicateDeclaration + | WarnCaseWithoutFlowControlEnd) , _lastValue(0) { } @@ -212,6 +361,17 @@ QList<DiagnosticMessage> Check::operator()() return _messages; } +bool Check::preVisit(Node *ast) +{ + _chain.append(ast); + return true; +} + +void Check::postVisit(Node *) +{ + _chain.removeLast(); +} + bool Check::visit(UiProgram *) { return true; @@ -372,6 +532,9 @@ bool Check::visit(FunctionDeclaration *ast) bool Check::visit(FunctionExpression *ast) { + FunctionBodyCheck bodyCheck; + _messages.append(bodyCheck(ast, _options)); + Node::accept(ast->formals, this); _scopeBuilder.push(ast); Node::accept(ast->body, this); @@ -379,6 +542,157 @@ bool Check::visit(FunctionExpression *ast) return false; } +static bool shouldAvoidNonStrictEqualityCheck(ExpressionNode *exp) +{ + if (NumericLiteral *literal = cast<NumericLiteral *>(exp)) { + if (literal->value == 0) + return true; + } else if (cast<TrueLiteral *>(exp) || cast<FalseLiteral *>(exp) || cast<NullExpression *>(exp)) { + return true; + } else if (IdentifierExpression *ident = cast<IdentifierExpression *>(exp)) { + if (ident->name && ident->name->asString() == QLatin1String("undefined")) + return true; + } else if (StringLiteral *literal = cast<StringLiteral *>(exp)) { + if (!literal->value || literal->value->asString().isEmpty()) + return true; + } + return false; +} + +bool Check::visit(BinaryExpression *ast) +{ + if (ast->op == QSOperator::Equal || ast->op == QSOperator::NotEqual) { + if (_options & WarnAllNonStrictEqualityChecks + || (_options & WarnDangerousNonStrictEqualityChecks + && (shouldAvoidNonStrictEqualityCheck(ast->left) + || shouldAvoidNonStrictEqualityCheck(ast->right)))) { + warning(ast->operatorToken, tr("== and != perform type coercion, use === or !== instead to avoid")); + } + } + return true; +} + +bool Check::visit(Block *ast) +{ + if (Node *p = parent()) { + if (_options & WarnBlocks + && !cast<UiScriptBinding *>(p) + && !cast<TryStatement *>(p) + && !cast<Catch *>(p) + && !cast<Finally *>(p) + && !cast<ForStatement *>(p) + && !cast<ForEachStatement *>(p) + && !cast<DoWhileStatement *>(p) + && !cast<WhileStatement *>(p) + && !cast<IfStatement *>(p) + && !cast<SwitchStatement *>(p) + && !cast<WithStatement *>(p)) { + warning(ast->lbraceToken, tr("blocks do not introduce a new scope, avoid")); + } + } + return true; +} + +bool Check::visit(WithStatement *ast) +{ + if (_options & WarnWith) + warning(ast->withToken, tr("use of the with statement is not recommended, use a var instead")); + return true; +} + +bool Check::visit(VoidExpression *ast) +{ + if (_options & WarnVoid) + warning(ast->voidToken, tr("use of void is usually confusing and not recommended")); + return true; +} + +bool Check::visit(Expression *ast) +{ + if (_options & WarnCommaExpression && ast->left && ast->right) { + if (!cast<ForStatement *>(parent())) + warning(ast->commaToken, tr("avoid comma expressions")); + } + return true; +} + +bool Check::visit(ExpressionStatement *ast) +{ + if (_options & WarnExpressionStatement && ast->expression) { + bool ok = cast<CallExpression *>(ast->expression) + || cast<DeleteExpression *>(ast->expression) + || cast<PreDecrementExpression *>(ast->expression) + || cast<PreIncrementExpression *>(ast->expression) + || cast<PostIncrementExpression *>(ast->expression) + || cast<PostDecrementExpression *>(ast->expression) + || cast<UiScriptBinding *>(parent()); + if (BinaryExpression *binary = cast<BinaryExpression *>(ast->expression)) { + switch (binary->op) { + case QSOperator::Assign: + case QSOperator::InplaceAdd: + case QSOperator::InplaceAnd: + case QSOperator::InplaceDiv: + case QSOperator::InplaceLeftShift: + case QSOperator::InplaceRightShift: + case QSOperator::InplaceMod: + case QSOperator::InplaceMul: + case QSOperator::InplaceOr: + case QSOperator::InplaceSub: + case QSOperator::InplaceURightShift: + case QSOperator::InplaceXor: + ok = true; + default: break; + } + } + + if (!ok) { + warning(locationFromRange(ast->firstSourceLocation(), ast->lastSourceLocation()), + tr("expression statements should be assignments, calls or delete expressions only")); + } + } + return true; +} + +bool Check::visit(IfStatement *ast) +{ + if (ast->expression) + checkAssignInCondition(ast->expression); + return true; +} + +bool Check::visit(ForStatement *ast) +{ + if (ast->condition) + checkAssignInCondition(ast->condition); + return true; +} + +bool Check::visit(WhileStatement *ast) +{ + if (ast->expression) + checkAssignInCondition(ast->expression); + return true; +} + +bool Check::visit(DoWhileStatement *ast) +{ + if (ast->expression) + checkAssignInCondition(ast->expression); + return true; +} + +bool Check::visit(CaseClause *ast) +{ + checkEndsWithControlFlow(ast->statements, ast->caseToken); + return true; +} + +bool Check::visit(DefaultClause *ast) +{ + checkEndsWithControlFlow(ast->statements, ast->defaultToken); + return true; +} + /// When something is changed here, also change ReadingContext::lookupProperty in /// texttomodelmerger.cpp /// ### Maybe put this into the context as a helper method. @@ -457,6 +771,34 @@ const Value *Check::checkScopeObjectMember(const UiQualifiedId *id) return value; } +void Check::checkAssignInCondition(AST::ExpressionNode *condition) +{ + if (_options & WarnAssignInCondition) { + if (BinaryExpression *binary = cast<BinaryExpression *>(condition)) { + if (binary->op == QSOperator::Assign) + warning(binary->operatorToken, tr("avoid assignments in conditions")); + } + } +} + +void Check::checkEndsWithControlFlow(StatementList *statements, SourceLocation errorLoc) +{ + // full flow analysis would be neat + if (!statements || !(_options & WarnCaseWithoutFlowControlEnd)) + return; + + Statement *lastStatement = 0; + for (StatementList *slist = statements; slist; slist = slist->next) + lastStatement = slist->statement; + + if (!cast<ReturnStatement *>(lastStatement) + && !cast<ThrowStatement *>(lastStatement) + && !cast<BreakStatement *>(lastStatement) + && !cast<ContinueStatement *>(lastStatement)) { + warning(errorLoc, tr("case does not end with return, break, continue or throw")); + } +} + void Check::error(const AST::SourceLocation &loc, const QString &message) { _messages.append(DiagnosticMessage(DiagnosticMessage::Error, loc, message)); @@ -466,3 +808,11 @@ void Check::warning(const AST::SourceLocation &loc, const QString &message) { _messages.append(DiagnosticMessage(DiagnosticMessage::Warning, loc, message)); } + +Node *Check::parent(int distance) +{ + const int index = _chain.size() - 2 - distance; + if (index < 0) + return 0; + return _chain.at(index); +} diff --git a/src/libs/qmljs/qmljscheck.h b/src/libs/qmljs/qmljscheck.h index 97297eac357..7230b1f886d 100644 --- a/src/libs/qmljs/qmljscheck.h +++ b/src/libs/qmljs/qmljscheck.h @@ -50,7 +50,26 @@ public: QList<DiagnosticMessage> operator()(); + enum Option { + WarnDangerousNonStrictEqualityChecks = 1 << 0, + WarnAllNonStrictEqualityChecks = 1 << 1, + WarnBlocks = 1 << 2, + WarnWith = 1 << 3, + WarnVoid = 1 << 4, + WarnCommaExpression = 1 << 5, + WarnExpressionStatement = 1 << 6, + WarnAssignInCondition = 1 << 7, + WarnUseBeforeDeclaration = 1 << 8, + WarnDuplicateDeclaration = 1 << 9, + WarnDeclarationsNotStartOfFunction = 1 << 10, + WarnCaseWithoutFlowControlEnd = 1 << 11, + }; + Q_DECLARE_FLAGS(Options, Option); + protected: + virtual bool preVisit(AST::Node *ast); + virtual void postVisit(AST::Node *ast); + virtual bool visit(AST::UiProgram *ast); virtual bool visit(AST::UiObjectDefinition *ast); virtual bool visit(AST::UiObjectBinding *ast); @@ -61,14 +80,31 @@ protected: virtual bool visit(AST::FunctionDeclaration *ast); virtual bool visit(AST::FunctionExpression *ast); + virtual bool visit(AST::BinaryExpression *ast); + virtual bool visit(AST::Block *ast); + virtual bool visit(AST::WithStatement *ast); + virtual bool visit(AST::VoidExpression *ast); + virtual bool visit(AST::Expression *ast); + virtual bool visit(AST::ExpressionStatement *ast); + virtual bool visit(AST::IfStatement *ast); + virtual bool visit(AST::ForStatement *ast); + virtual bool visit(AST::WhileStatement *ast); + virtual bool visit(AST::DoWhileStatement *ast); + virtual bool visit(AST::CaseClause *ast); + virtual bool visit(AST::DefaultClause *ast); + private: void visitQmlObject(AST::Node *ast, AST::UiQualifiedId *typeId, AST::UiObjectInitializer *initializer); const Interpreter::Value *checkScopeObjectMember(const AST::UiQualifiedId *id); + void checkAssignInCondition(AST::ExpressionNode *condition); + void checkEndsWithControlFlow(AST::StatementList *statements, AST::SourceLocation errorLoc); void warning(const AST::SourceLocation &loc, const QString &message); void error(const AST::SourceLocation &loc, const QString &message); + AST::Node *parent(int distance = 0); + Document::Ptr _doc; Snapshot _snapshot; @@ -78,8 +114,10 @@ private: QList<DiagnosticMessage> _messages; bool _ignoreTypeErrors; + Options _options; const Interpreter::Value *_lastValue; + QList<AST::Node *> _chain; }; QMLJS_EXPORT QColor toQColor(const QString &qmlColorString); -- GitLab