/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** ** GNU Lesser General Public License Usage ** ** This file may be used under the terms of the GNU Lesser General Public ** License version 2.1 as published by the Free Software Foundation and ** appearing in the file LICENSE.LGPL included in the packaging of this file. ** Please review the following information to ensure the GNU Lesser General ** Public License version 2.1 requirements will be met: ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** Other Usage ** ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** **************************************************************************/ #include "qmljsfindreferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qmljseditorconstants.h" #include #include #include #include #include #include #include #include #include using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJSEditor; namespace { // ### These visitors could be useful in general class FindUsages: protected Visitor { public: typedef QList Result; FindUsages(Document::Ptr doc, const ContextPtr &context) : _doc(doc) , _scopeChain(doc, context) , _builder(&_scopeChain) { } Result operator()(const QString &name, const ObjectValue *scope) { _name = name; _scope = scope; _usages.clear(); if (_doc) Node::accept(_doc->ast(), this); return _usages; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; virtual bool visit(AST::UiPublicMember *node) { if (node->name == _name && _scopeChain.qmlScopeObjects().contains(_scope)) { _usages.append(node->identifierToken); } if (AST::cast(node->statement)) { _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } virtual bool visit(AST::UiObjectDefinition *node) { _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } virtual bool visit(AST::UiObjectBinding *node) { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } virtual bool visit(AST::UiScriptBinding *node) { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } if (AST::cast(node->statement)) { Node::accept(node->qualifiedId, this); _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } virtual bool visit(AST::UiArrayBinding *node) { if (node->qualifiedId && !node->qualifiedId->next && node->qualifiedId->name == _name && checkQmlScope()) { _usages.append(node->qualifiedId->identifierToken); } return true; } virtual bool visit(AST::IdentifierExpression *node) { if (node->name.isEmpty() || node->name != _name) return false; const ObjectValue *scope; _scopeChain.lookup(_name, &scope); if (!scope) return false; if (check(scope)) { _usages.append(node->identifierToken); return false; } // the order of scopes in 'instantiatingComponents' is undefined, // so it might still be a use - we just found a different value in a different scope first // if scope is one of these, our match wasn't inside the instantiating components list const ScopeChain &chain = _scopeChain; if (chain.jsScopes().contains(scope) || chain.qmlScopeObjects().contains(scope) || chain.qmlTypes() == scope || chain.globalScope() == scope) return false; if (contains(chain.qmlComponentChain().data())) _usages.append(node->identifierToken); return false; } virtual bool visit(AST::FieldMemberExpression *node) { if (node->name != _name) return true; Evaluate evaluate(&_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; if (check(lhsValue->asObjectValue())) // passing null is ok _usages.append(node->identifierToken); return true; } virtual bool visit(AST::FunctionDeclaration *node) { return visit(static_cast(node)); } virtual bool visit(AST::FunctionExpression *node) { if (node->name == _name) { if (checkLookup()) _usages.append(node->identifierToken); } Node::accept(node->formals, this); _builder.push(node); Node::accept(node->body, this); _builder.pop(); return false; } virtual bool visit(AST::VariableDeclaration *node) { if (node->name == _name) { if (checkLookup()) _usages.append(node->identifierToken); } return true; } private: bool contains(const QmlComponentChain *chain) { if (!chain || !chain->document() || !chain->document()->bind()) return false; const ObjectValue *idEnv = chain->document()->bind()->idEnvironment(); if (idEnv && idEnv->lookupMember(_name, _scopeChain.context())) return idEnv == _scope; const ObjectValue *root = chain->document()->bind()->rootObjectValue(); if (root && root->lookupMember(_name, _scopeChain.context())) { return check(root); } foreach (const QmlComponentChain *parent, chain->instantiatingComponents()) { if (contains(parent)) return true; } return false; } bool check(const ObjectValue *s) { if (!s) return false; const ObjectValue *definingObject; s->lookupMember(_name, _scopeChain.context(), &definingObject); return definingObject == _scope; } bool checkQmlScope() { foreach (const ObjectValue *s, _scopeChain.qmlScopeObjects()) { if (check(s)) return true; } return false; } bool checkLookup() { const ObjectValue *scope = 0; _scopeChain.lookup(_name, &scope); return check(scope); } Result _usages; Document::Ptr _doc; ScopeChain _scopeChain; ScopeBuilder _builder; QString _name; const ObjectValue *_scope; }; class FindTypeUsages: protected Visitor { public: typedef QList Result; FindTypeUsages(Document::Ptr doc, const ContextPtr &context) : _doc(doc) , _context(context) , _scopeChain(doc, context) , _builder(&_scopeChain) { } Result operator()(const QString &name, const ObjectValue *typeValue) { _name = name; _typeValue = typeValue; _usages.clear(); if (_doc) Node::accept(_doc->ast(), this); return _usages; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; virtual bool visit(AST::UiPublicMember *node) { if (node->memberType == _name){ const ObjectValue * tVal = _context->lookupType(_doc.data(), QStringList(_name)); if (tVal == _typeValue) _usages.append(node->typeToken); } if (AST::cast(node->statement)) { _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } virtual bool visit(AST::UiObjectDefinition *node) { checkTypeName(node->qualifiedTypeNameId); _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } virtual bool visit(AST::UiObjectBinding *node) { checkTypeName(node->qualifiedTypeNameId); _builder.push(node); Node::accept(node->initializer, this); _builder.pop(); return false; } virtual bool visit(AST::UiScriptBinding *node) { if (AST::cast(node->statement)) { Node::accept(node->qualifiedId, this); _builder.push(node); Node::accept(node->statement, this); _builder.pop(); return false; } return true; } virtual bool visit(AST::IdentifierExpression *node) { if (node->name != _name) return false; const ObjectValue *scope; const Value *objV = _scopeChain.lookup(_name, &scope); if (objV == _typeValue) _usages.append(node->identifierToken); return false; } virtual bool visit(AST::FieldMemberExpression *node) { if (node->name != _name) return true; Evaluate evaluate(&_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; const ObjectValue *lhsObj = lhsValue->asObjectValue(); if (lhsObj && lhsObj->lookupMember(_name, _context) == _typeValue) _usages.append(node->identifierToken); return true; } virtual bool visit(AST::FunctionDeclaration *node) { return visit(static_cast(node)); } virtual bool visit(AST::FunctionExpression *node) { Node::accept(node->formals, this); _builder.push(node); Node::accept(node->body, this); _builder.pop(); return false; } virtual bool visit(AST::VariableDeclaration *node) { Node::accept(node->expression, this); return false; } virtual bool visit(UiImport *ast) { if (ast && ast->importId == _name) { const Imports *imp = _context->imports(_doc.data()); if (!imp) return false; if (_context->lookupType(_doc.data(), QStringList(_name)) == _typeValue) _usages.append(ast->importIdToken); } return false; } private: bool checkTypeName(UiQualifiedId *id) { for (UiQualifiedId *att = id; att; att = att->next){ if (att->name == _name) { const ObjectValue *objectValue = _context->lookupType(_doc.data(), id, att->next); if (_typeValue == objectValue){ _usages.append(att->identifierToken); return true; } } } return false; } Result _usages; Document::Ptr _doc; ContextPtr _context; ScopeChain _scopeChain; ScopeBuilder _builder; QString _name; const ObjectValue *_typeValue; }; class FindTargetExpression: protected Visitor { public: enum Kind { ExpKind, TypeKind }; FindTargetExpression(Document::Ptr doc, const ScopeChain *scopeChain) : _doc(doc), _scopeChain(scopeChain) { } void operator()(quint32 offset) { _name.clear(); _scope = 0; _objectNode = 0; _offset = offset; _typeKind = ExpKind; if (_doc) Node::accept(_doc->ast(), this); } QString name() const { return _name; } const ObjectValue *scope() { if (!_scope) _scopeChain->lookup(_name, &_scope); return _scope; } Kind typeKind(){ return _typeKind; } const Value *targetValue(){ return _targetValue; } protected: void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; virtual bool preVisit(Node *node) { if (Statement *stmt = node->statementCast()) { return containsOffset(stmt->firstSourceLocation(), stmt->lastSourceLocation()); } else if (ExpressionNode *exp = node->expressionCast()) { return containsOffset(exp->firstSourceLocation(), exp->lastSourceLocation()); } else if (UiObjectMember *ui = node->uiObjectMemberCast()) { return containsOffset(ui->firstSourceLocation(), ui->lastSourceLocation()); } return true; } virtual bool visit(IdentifierExpression *node) { if (containsOffset(node->identifierToken)) { _name = node->name.toString(); if ((!_name.isEmpty()) && _name.at(0).isUpper()) { // a possible type _targetValue = _scopeChain->lookup(_name, &_scope); if (value_cast(_targetValue)) _typeKind = TypeKind; } } return true; } virtual bool visit(FieldMemberExpression *node) { if (containsOffset(node->identifierToken)) { setScope(node->base); _name = node->name.toString(); if ((!_name.isEmpty()) && _name.at(0).isUpper()) { // a possible type Evaluate evaluate(_scopeChain); const Value *lhsValue = evaluate(node->base); if (!lhsValue) return true; const ObjectValue *lhsObj = lhsValue->asObjectValue(); if (lhsObj) { _scope = lhsObj; _targetValue = lhsObj->lookupMember(_name, _scopeChain->context()); _typeKind = TypeKind; } } return false; } return true; } virtual bool visit(UiScriptBinding *node) { return !checkBindingName(node->qualifiedId); } virtual bool visit(UiArrayBinding *node) { return !checkBindingName(node->qualifiedId); } virtual bool visit(UiObjectBinding *node) { if ((!checkTypeName(node->qualifiedTypeNameId)) && (!checkBindingName(node->qualifiedId))) { Node *oldObjectNode = _objectNode; _objectNode = node; accept(node->initializer); _objectNode = oldObjectNode; } return false; } virtual bool visit(UiObjectDefinition *node) { if (!checkTypeName(node->qualifiedTypeNameId)) { Node *oldObjectNode = _objectNode; _objectNode = node; accept(node->initializer); _objectNode = oldObjectNode; } return false; } virtual bool visit(UiPublicMember *node) { if (containsOffset(node->typeToken)){ if (!node->memberType.isEmpty()) { _name = node->memberType.toString(); _targetValue = _scopeChain->context()->lookupType(_doc.data(), QStringList(_name)); _scope = 0; _typeKind = TypeKind; } return false; } else if (containsOffset(node->identifierToken)) { _scope = _doc->bind()->findQmlObject(_objectNode); _name = node->name.toString(); return false; } return true; } virtual bool visit(FunctionDeclaration *node) { return visit(static_cast(node)); } virtual bool visit(FunctionExpression *node) { if (containsOffset(node->identifierToken)) { _name = node->name.toString(); return false; } return true; } virtual bool visit(VariableDeclaration *node) { if (containsOffset(node->identifierToken)) { _name = node->name.toString(); return false; } return true; } private: bool containsOffset(SourceLocation start, SourceLocation end) { return _offset >= start.begin() && _offset <= end.end(); } bool containsOffset(SourceLocation loc) { return _offset >= loc.begin() && _offset <= loc.end(); } bool checkBindingName(UiQualifiedId *id) { if (id && !id->name.isEmpty() && !id->next && containsOffset(id->identifierToken)) { _scope = _doc->bind()->findQmlObject(_objectNode); _name = id->name.toString(); return true; } return false; } bool checkTypeName(UiQualifiedId *id) { for (UiQualifiedId *att = id; att; att = att->next) { if (!att->name.isEmpty() && containsOffset(att->identifierToken)) { _targetValue = _scopeChain->context()->lookupType(_doc.data(), id, att->next); _scope = 0; _name = att->name.toString(); _typeKind = TypeKind; return true; } } return false; } void setScope(Node *node) { Evaluate evaluate(_scopeChain); const Value *v = evaluate(node); if (v) _scope = v->asObjectValue(); } QString _name; const ObjectValue *_scope; const Value *_targetValue; Node *_objectNode; Document::Ptr _doc; const ScopeChain *_scopeChain; quint32 _offset; Kind _typeKind; }; static QString matchingLine(unsigned position, const QString &source) { int start = source.lastIndexOf(QLatin1Char('\n'), position); start += 1; int end = source.indexOf(QLatin1Char('\n'), position); return source.mid(start, end - start); } class ProcessFile: public std::unary_function > { ContextPtr context; typedef FindReferences::Usage Usage; QString name; const ObjectValue *scope; public: ProcessFile(const ContextPtr &context, QString name, const ObjectValue *scope) : context(context), name(name), scope(scope) { } QList operator()(const QString &fileName) { QList usages; Document::Ptr doc = context->snapshot().document(fileName); if (!doc) return usages; // find all idenfifier expressions, try to resolve them and check if the result is in scope FindUsages findUsages(doc, context); FindUsages::Result results = findUsages(name, scope); foreach (const AST::SourceLocation &loc, results) usages.append(Usage(fileName, matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); return usages; } }; class SearchFileForType: public std::unary_function > { ContextPtr context; typedef FindReferences::Usage Usage; QString name; const ObjectValue *scope; public: SearchFileForType(const ContextPtr &context, QString name, const ObjectValue *scope) : context(context), name(name), scope(scope) { } QList operator()(const QString &fileName) { QList usages; Document::Ptr doc = context->snapshot().document(fileName); if (!doc) return usages; // find all idenfifier expressions, try to resolve them and check if the result is in scope FindTypeUsages findUsages(doc, context); FindTypeUsages::Result results = findUsages(name, scope); foreach (const AST::SourceLocation &loc, results) usages.append(Usage(fileName, matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); return usages; } }; class UpdateUI: public std::binary_function &, QList, void> { typedef FindReferences::Usage Usage; QFutureInterface *future; public: UpdateUI(QFutureInterface *future): future(future) {} void operator()(QList &, const QList &usages) { foreach (const Usage &u, usages) future->reportResult(u); future->setProgressValue(future->progressValue() + 1); } }; } // end of anonymous namespace FindReferences::FindReferences(QObject *parent) : QObject(parent) { m_watcher.setPendingResultsLimit(1); connect(&m_watcher, SIGNAL(resultsReadyAt(int,int)), this, SLOT(displayResults(int,int))); connect(&m_watcher, SIGNAL(finished()), this, SLOT(searchFinished())); } FindReferences::~FindReferences() { } static void find_helper(QFutureInterface &future, const ModelManagerInterface::WorkingCopy workingCopy, Snapshot snapshot, const QString fileName, quint32 offset, QString replacement) { // update snapshot from workingCopy to make sure it's up to date // ### remove? // ### this is a great candidate for map-reduce QHashIterator< QString, QPair > it(workingCopy.all()); while (it.hasNext()) { it.next(); const QString fileName = it.key(); Document::Ptr oldDoc = snapshot.document(fileName); if (oldDoc && oldDoc->editorRevision() == it.value().second) continue; Document::Language language; if (oldDoc) language = oldDoc->language(); else language = QmlJSTools::languageOfFile(fileName); Document::Ptr newDoc = snapshot.documentFromSource(it.value().first, fileName, language); newDoc->parse(); snapshot.insert(newDoc); } // find the scope for the name we're searching Document::Ptr doc = snapshot.document(fileName); if (!doc) return; QmlJS::ModelManagerInterface *modelManager = QmlJS::ModelManagerInterface::instance(); Link link(snapshot, modelManager->importPaths(), modelManager->builtins(doc)); ContextPtr context = link(); ScopeChain scopeChain(doc, context); ScopeBuilder builder(&scopeChain); ScopeAstPath astPath(doc); builder.push(astPath(offset)); FindTargetExpression findTarget(doc, &scopeChain); findTarget(offset); const QString &name = findTarget.name(); if (name.isEmpty()) return; if (!replacement.isNull() && replacement.isEmpty()) replacement = name; QStringList files; foreach (const Document::Ptr &doc, snapshot) { // ### skip files that don't contain the name token files.append(doc->fileName()); } future.setProgressRange(0, files.size()); // report a dummy usage to indicate the search is starting FindReferences::Usage searchStarting(replacement, name, 0, 0, 0); if (findTarget.typeKind() == findTarget.TypeKind){ const ObjectValue *typeValue = value_cast(findTarget.targetValue()); if (!typeValue) return; future.reportResult(searchStarting); SearchFileForType process(context, name, typeValue); UpdateUI reduce(&future); QtConcurrent::blockingMappedReduced > (files, process, reduce); } else { const ObjectValue *scope = findTarget.scope(); if (!scope) return; scope->lookupMember(name, context, &scope); if (!scope) return; if (!scope->className().isEmpty()) searchStarting.lineText.prepend(scope->className() + QLatin1Char('.')); future.reportResult(searchStarting); ProcessFile process(context, name, scope); UpdateUI reduce(&future); QtConcurrent::blockingMappedReduced > (files, process, reduce); } future.setProgressValue(files.size()); } void FindReferences::findUsages(const QString &fileName, quint32 offset) { ModelManagerInterface *modelManager = ModelManagerInterface::instance(); QFuture result = QtConcurrent::run( &find_helper, modelManager->workingCopy(), modelManager->snapshot(), fileName, offset, QString()); m_watcher.setFuture(result); } void FindReferences::renameUsages(const QString &fileName, quint32 offset, const QString &replacement) { ModelManagerInterface *modelManager = ModelManagerInterface::instance(); // an empty non-null string asks the future to use the current name as base QString newName = replacement; if (newName.isNull()) newName = QLatin1String(""); QFuture result = QtConcurrent::run( &find_helper, modelManager->workingCopy(), modelManager->snapshot(), fileName, offset, newName); m_watcher.setFuture(result); } void FindReferences::displayResults(int first, int last) { // the first usage is always a dummy to indicate we now start searching if (first == 0) { Usage dummy = m_watcher.future().resultAt(0); const QString replacement = dummy.path; const QString symbolName = dummy.lineText; const QString label = tr("QML/JS Usages:"); if (replacement.isEmpty()) { m_currentSearch = Find::SearchResultWindow::instance()->startNewSearch( label, QString(), symbolName, Find::SearchResultWindow::SearchOnly); } else { m_currentSearch = Find::SearchResultWindow::instance()->startNewSearch( label, QString(), symbolName, Find::SearchResultWindow::SearchAndReplace); m_currentSearch->setTextToReplace(replacement); connect(m_currentSearch, SIGNAL(replaceButtonClicked(QString,QList)), SLOT(onReplaceButtonClicked(QString,QList))); } connect(m_currentSearch, SIGNAL(activated(Find::SearchResultItem)), this, SLOT(openEditor(Find::SearchResultItem))); connect(m_currentSearch, SIGNAL(cancelled()), this, SLOT(cancel())); Find::SearchResultWindow::instance()->popup(true); Core::ProgressManager *progressManager = Core::ICore::instance()->progressManager(); Core::FutureProgress *progress = progressManager->addTask( m_watcher.future(), tr("Searching"), QmlJSEditor::Constants::TASK_SEARCH); connect(progress, SIGNAL(clicked()), Find::SearchResultWindow::instance(), SLOT(popup())); ++first; } if (!m_currentSearch) { m_watcher.cancel(); return; } for (int index = first; index != last; ++index) { Usage result = m_watcher.future().resultAt(index); m_currentSearch->addResult(result.path, result.line, result.lineText, result.col, result.len); } } void FindReferences::searchFinished() { if (m_currentSearch) m_currentSearch->finishSearch(); m_currentSearch = 0; emit changed(); } void FindReferences::cancel() { m_watcher.cancel(); } void FindReferences::openEditor(const Find::SearchResultItem &item) { if (item.path.size() > 0) { TextEditor::BaseTextEditorWidget::openEditorAt(item.path.first(), item.lineNumber, item.textMarkPos, QString(), Core::EditorManager::ModeSwitch); } else { Core::EditorManager::instance()->openEditor(item.text, QString(), Core::EditorManager::ModeSwitch); } } void FindReferences::onReplaceButtonClicked(const QString &text, const QList &items) { const QStringList fileNames = TextEditor::BaseFileFind::replaceAll(text, items); // files that are opened in an editor are changed, but not saved QStringList changedOnDisk; QStringList changedUnsavedEditors; Core::EditorManager *editorManager = Core::EditorManager::instance(); foreach (const QString &fileName, fileNames) { if (editorManager->editorsForFileName(fileName).isEmpty()) changedOnDisk += fileName; else changedUnsavedEditors += fileName; } if (!changedOnDisk.isEmpty()) QmlJS::ModelManagerInterface::instance()->updateSourceFiles(changedOnDisk, true); if (!changedUnsavedEditors.isEmpty()) QmlJS::ModelManagerInterface::instance()->updateSourceFiles(changedUnsavedEditors, false); Find::SearchResultWindow::instance()->hide(); }