diff --git a/duchain/tests/pyduchaintest.cpp b/duchain/tests/pyduchaintest.cpp --- a/duchain/tests/pyduchaintest.cpp +++ b/duchain/tests/pyduchaintest.cpp @@ -704,7 +704,7 @@ visitor->searchingForRange = r; visitor->searchingForIdentifier = identifier; visitor->visitCode(m_ast.data()); - + QEXPECT_FAIL("attr_dot_name_hash", "Insufficiently magic hack", Continue); QCOMPARE(visitor->found, true); delete visitor; } @@ -733,6 +733,11 @@ QTest::newRow("attr_of_string_in_list") << "[\"*{0}*\".format(foo)]" << 1 << ( QStringList() << "9,14,format" ); QTest::newRow("attr_of_call_in_list") << "[foo().format(foo)]" << 1 << ( QStringList() << "7,12,format" ); QTest::newRow("attr_parentheses") << "(\"foo\" + \"foo\").capitalize()" << 1 << ( QStringList() << "16,25,capitalize" ); + QTest::newRow("attr_commented_name") << "base.attr # attr" << 2 << ( QStringList() << "5,8,attr" ); + QTest::newRow("attr_name_in_strings") << "'attr' + base['attr'].attr # attr" << 4 << ( QStringList() << "22,25,attr" ); + QTest::newRow("attr_dot_hash_in_strings") << "'.foo#' + base['.#'].attr # attr" << 4 << ( QStringList() << "21,24,attr" ); + QTest::newRow("attr_dot_name_hash") << "base['.attr#'].attr" << 4 << ( QStringList() << "15,18,attr" ); + QTest::newRow("string_parentheses") << "(\"asdf\".join())" << 1 << ( QStringList() << "8,11,join" ); QTest::newRow("string_parentheses2") << "(\"asdf\").join()" << 1 << ( QStringList() << "9,12,join" ); QTest::newRow("string_parentheses3") << "(\"asdf\".join()).join()" << 2 << ( QStringList() << "8,11,join" << "16,19,join" ); diff --git a/parser/astbuilder.cpp b/parser/astbuilder.cpp --- a/parser/astbuilder.cpp +++ b/parser/astbuilder.cpp @@ -135,30 +135,66 @@ } // take only the portion of the line up to that next expression - QString line = lines.at(node->startLine); - auto lineno = next_start.line(); - auto colno = next_start.column(); + auto endLine = next_start.line(); + auto endCol = next_start.column(); if ( ! (next_start > node->start()) ) { - colno = -1; - lineno = node->startLine; + endLine = node->startLine; + endCol = -1; } - do { - line = lines.at(lineno); - if ( colno != -1 ) { - line = line.left(colno); + + const QString& name(node->attribute->value); + + QString line; + for ( int n = node->startLine, + pos = node->value->endCol + 1, + dotFound = false, + nameFound = false; + n <= endLine; ++n, pos = 0 ) { + line = lines.at(n); + if ( n == endLine && endCol != -1 ) { + // Never look at the next expression. + line = line.left(endCol); + } + if ( !dotFound ) { + // The real attr name can never be before a dot. + // Nor can the start of a comment. + // (Don't be misled by `foo["bar"].bar` or `foo["#"].bar`) + pos = line.indexOf('.', pos); + if ( pos == -1 ) continue; + dotFound = true; + } + if ( !nameFound ) { + // Track if the attr name has appeared at least once. + // This helps avoid interpreting '#'s in strings as comments - + // there can never be a comment before the real attr name. + pos = line.indexOf(name, pos + 1); + if ( pos == -1 ) continue; + nameFound = true; } - colno = -1; - lineno -= 1; - // eventually go to previous line for multi-line expressions - } while ( ! line.contains(node->attribute->value) && lineno >= 0 ); + if ( dotFound && nameFound && + (pos = line.indexOf('#', pos + name.length())) != -1) { + // Remove the comment after a '#' iff we're certain it can't + // be inside a string literal (e.g. `foo["#"].bar`). + line = line.left(pos); + } + // Take the last occurrence, any others are in string literals. + pos = line.lastIndexOf(name); + if ( pos != -1 ) { + node->startLine = n; + node->startCol = pos; + } + // N.B. we do this for all lines, the last non-comment occurrence + // is the real one. + } + // This fails (only, AFAIK) in a very limited case: + // If the value expression (`foo` in `foo.bar`) contains a dot, the + // attr name, _and_ a hash in that order (may not be consecutive), + // and the hash is on the same line as the real attr name, + // we wrongly interpret the hash as the start of a comment. + // e.g `foo["...barrier#"].bar` will highlight part of the string. - node->startLine = lineno + 1; node->endLine = node->startLine; - // now, just take the last occurrence of the value. - // it is guaranteed that this is the right one, otherwise - // that portion of the line would have been cut off above. - node->startCol = line.lastIndexOf(node->attribute->value); - node->endCol = node->startCol + node->attribute->value.length() - 1; + node->endCol = node->startCol + name.length() - 1; node->attribute->copyRange(node); AstDefaultVisitor::visitAttribute(node);