blob: a8a7a157a7d8e8d7b5681ceac73f0031fc7486c9 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/legend.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SemanticTokensTest);
});
}
@reflectiveTest
class SemanticTokensTest extends AbstractLspAnalysisServerTest {
/// Decode tokens according to the LSP spec and pair with relevant file contents.
List<_Token> decodeSemanticTokens(String content, SemanticTokens tokens) {
final contentLines = content.split('\n').map((line) => '$line\n').toList();
final results = <_Token>[];
var lastLine = 0;
var lastColumn = 0;
for (var i = 0; i < tokens.data.length; i += 5) {
final lineDelta = tokens.data[i];
final columnDelta = tokens.data[i + 1];
final length = tokens.data[i + 2];
final tokenTypeIndex = tokens.data[i + 3];
final modifierBitmask = tokens.data[i + 4];
// Calculate the actual line/col from the deltas.
final line = lastLine + lineDelta;
final column = lineDelta == 0 ? lastColumn + columnDelta : columnDelta;
final tokenContent =
contentLines[line].substring(column, column + length);
results.add(_Token(
tokenContent,
semanticTokenLegend.typeForIndex(tokenTypeIndex),
semanticTokenLegend.modifiersForBitmask(modifierBitmask),
));
lastLine = line;
lastColumn = column;
}
return results;
}
Future<void> test_class() async {
final content = '''
/// class docs
class MyClass<T> {
// class comment
}
// Trailing comment
''';
final expected = [
_Token('/// class docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('T', SemanticTokenTypes.typeParameter),
_Token('// class comment', SemanticTokenTypes.comment),
_Token('// Trailing comment', SemanticTokenTypes.comment),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_class_fields() async {
final content = '''
class MyClass {
/// field docs
String myField = 'FieldVal';
/// static field docs
static String myStaticField = 'StaticFieldVal';
}
main() {
final a = MyClass();
print(a.myField);
MyClass.myStaticField = 'a';
}
''';
final expected = [
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('/// field docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('String', SemanticTokenTypes.class_),
_Token('myField', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token("'FieldVal'", SemanticTokenTypes.string),
_Token('/// static field docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('static', SemanticTokenTypes.keyword),
_Token('String', SemanticTokenTypes.class_),
_Token('myStaticField', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token("'StaticFieldVal'", SemanticTokenTypes.string),
_Token('main', SemanticTokenTypes.function,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('final', SemanticTokenTypes.keyword),
_Token('a', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('print', SemanticTokenTypes.function),
_Token('a', SemanticTokenTypes.variable),
_Token('myField', SemanticTokenTypes.property),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('myStaticField', SemanticTokenTypes.property,
[SemanticTokenModifiers.static]),
_Token("'a'", SemanticTokenTypes.string),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_class_getterSetter() async {
final content = '''
class MyClass {
/// getter docs
String get myGetter => 'GetterVal';
/// setter docs
set mySetter(String v) {};
/// static getter docs
static String get myStaticGetter => 'StaticGetterVal';
/// static setter docs
static set myStaticSetter(String staticV) {};
}
main() {
final a = MyClass();
print(a.myGetter);
a.mySetter = 'a';
}
''';
final expected = [
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('/// getter docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('String', SemanticTokenTypes.class_),
_Token('get', SemanticTokenTypes.keyword),
_Token('myGetter', SemanticTokenTypes.property,
[SemanticTokenModifiers.declaration]),
_Token("'GetterVal'", SemanticTokenTypes.string),
_Token('/// setter docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('set', SemanticTokenTypes.keyword),
_Token('mySetter', SemanticTokenTypes.property,
[SemanticTokenModifiers.declaration]),
_Token('String', SemanticTokenTypes.class_),
_Token('v', SemanticTokenTypes.parameter,
[SemanticTokenModifiers.declaration]),
_Token('/// static getter docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('static', SemanticTokenTypes.keyword),
_Token('String', SemanticTokenTypes.class_),
_Token('get', SemanticTokenTypes.keyword),
_Token('myStaticGetter', SemanticTokenTypes.property,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token("'StaticGetterVal'", SemanticTokenTypes.string),
_Token('/// static setter docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('static', SemanticTokenTypes.keyword),
_Token('set', SemanticTokenTypes.keyword),
_Token('myStaticSetter', SemanticTokenTypes.property,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('String', SemanticTokenTypes.class_),
_Token('staticV', SemanticTokenTypes.parameter,
[SemanticTokenModifiers.declaration]),
_Token('main', SemanticTokenTypes.function,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('final', SemanticTokenTypes.keyword),
_Token('a', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('print', SemanticTokenTypes.function),
_Token('a', SemanticTokenTypes.variable),
_Token('myGetter', SemanticTokenTypes.property),
_Token('a', SemanticTokenTypes.variable),
_Token('mySetter', SemanticTokenTypes.property),
_Token("'a'", SemanticTokenTypes.string),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_class_method() async {
final content = '''
class MyClass {
/// method docs
@override
void myMethod() {}
/// static method docs
static void myStaticMethod() {
// static method comment
}
}
main() {
final a = MyClass();
a.myMethod();
MyClass.myStaticMethod();
}
''';
final expected = [
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('/// method docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('@', CustomSemanticTokenTypes.annotation),
_Token('override', SemanticTokenTypes.property),
_Token('void', SemanticTokenTypes.keyword),
_Token('myMethod', SemanticTokenTypes.method,
[SemanticTokenModifiers.declaration]),
_Token('/// static method docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('static', SemanticTokenTypes.keyword),
_Token('void', SemanticTokenTypes.keyword),
_Token('myStaticMethod', SemanticTokenTypes.method,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('// static method comment', SemanticTokenTypes.comment),
_Token('main', SemanticTokenTypes.function,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('final', SemanticTokenTypes.keyword),
_Token('a', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('a', SemanticTokenTypes.variable),
_Token('myMethod', SemanticTokenTypes.method),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('myStaticMethod', SemanticTokenTypes.method,
[SemanticTokenModifiers.static]),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_directives() async {
final content = '''
import 'package:flutter/material.dart';
export 'package:flutter/widgets.dart';
import '../file.dart';
library foo;
''';
final expected = [
_Token('import', SemanticTokenTypes.keyword),
_Token("'package:flutter/material.dart'", SemanticTokenTypes.string),
_Token('export', SemanticTokenTypes.keyword),
_Token("'package:flutter/widgets.dart'", SemanticTokenTypes.string),
_Token('import', SemanticTokenTypes.keyword),
_Token("'../file.dart'", SemanticTokenTypes.string),
_Token('library', SemanticTokenTypes.keyword),
_Token('foo', SemanticTokenTypes.namespace),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_fromPlugin() async {
final pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.foo');
final pluginAnalyzedFileUri = Uri.file(pluginAnalyzedFilePath);
final content = 'CLASS STRING VARIABLE';
final expected = [
_Token('CLASS', SemanticTokenTypes.class_),
_Token('STRING', SemanticTokenTypes.string),
_Token('VARIABLE', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
];
await initialize();
await openFile(pluginAnalyzedFileUri, withoutMarkers(content));
final pluginResult = plugin.AnalysisHighlightsParams(
pluginAnalyzedFilePath,
[
plugin.HighlightRegion(plugin.HighlightRegionType.CLASS, 0, 5),
plugin.HighlightRegion(plugin.HighlightRegionType.LITERAL_STRING, 6, 6),
plugin.HighlightRegion(
plugin.HighlightRegionType.TOP_LEVEL_VARIABLE_DECLARATION, 13, 8),
],
);
configureTestPlugin(notification: pluginResult.toNotification());
final tokens = await getSemanticTokens(pluginAnalyzedFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_invalidSyntax() async {
final content = '''
/// class docs
class MyClass {
// class comment
}
this is not valid code.
/// class docs 2
class MyClass2 {
// class comment 2
}
''';
// Expect toe correct tokens for the valid code before/after but don't
// check the the tokens for the invalid code as thre are no concrete
// expectations for them.
final expected1 = [
_Token('/// class docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
_Token('// class comment', SemanticTokenTypes.comment),
];
final expected2 = [
_Token('/// class docs 2', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass2', SemanticTokenTypes.class_),
_Token('// class comment 2', SemanticTokenTypes.comment),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
// Remove the tokens between the two expected sets.
decoded.removeRange(expected1.length, decoded.length - expected2.length);
expect(decoded, equals([...expected1, ...expected2]));
}
Future<void> test_lastLine_code() async {
final content = 'String var;';
final expected = [
_Token('String', SemanticTokenTypes.class_),
_Token('var', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_lastLine_comment() async {
final content = '// Trailing comment';
final expected = [
_Token('// Trailing comment', SemanticTokenTypes.comment),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_lastLine_multilineComment() async {
final content = '''/**
* Trailing comment
*/''';
final expected = [
_Token('/**', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('* Trailing comment', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('*/', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_manyBools_bug() async {
// Similar to test_manyImports_sortBug, this code triggered inconsistent tokens
// for "false" because tokens were sorted incorrectly (because both boolean and
// keyword had the same offset and length, which is all that were sorted by).
final content = '''
class MyTestClass {
/// test
/// test
bool test1 = false;
/// test
/// test
bool test2 = false;
/// test
/// test
bool test3 = false;
/// test
/// test
bool test4 = false;
/// test
/// test
bool test5 = false;
/// test
/// test
bool test6 = false;
}
''';
final expected = [
_Token('class', SemanticTokenTypes.keyword),
_Token('MyTestClass', SemanticTokenTypes.class_),
for (var i = 1; i <= 6; i++) ...[
_Token('/// test', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('/// test', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('bool', SemanticTokenTypes.class_),
_Token('test$i', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token('false', CustomSemanticTokenTypes.boolean),
],
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_manyImports_sortBug() async {
// This test is for a bug where some "import" tokens would not be highlighted
// correctly. Imports are made up of a DIRECTIVE token that spans a
// BUILT_IN ("import") and LITERAL_STRING. The original code sorted by only
// offset when handling overlapping tokens, which for certain lists (such as
// the one created for the code below) would result in the BUILTIN coming before
// the DIRECTIVE, which resulted in the DIRECTIVE overwriting it.
final content = '''
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
import 'dart:async';
''';
final expected = [
for (var i = 0; i < 13; i++) ...[
_Token('import', SemanticTokenTypes.keyword),
_Token("'dart:async'", SemanticTokenTypes.string),
],
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_multilineRegions() async {
final content = '''
/**
* This is my class comment
*
* There are
* multiple lines
*/
class MyClass {}
''';
final expected = [
_Token('/**', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('* This is my class comment', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('*', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('* There are', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('* multiple lines', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('*/', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('class', SemanticTokenTypes.keyword),
_Token('MyClass', SemanticTokenTypes.class_),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_strings() async {
final content = r'''
String foo(String c) => c;
const string1 = 'test';
const string2 = 'test1 $string1 test2 ${foo('test3')}';
const string3 = r'$string1 ${string1.length}';
''';
final expected = [
_Token('String', SemanticTokenTypes.class_),
_Token('foo', SemanticTokenTypes.function,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('String', SemanticTokenTypes.class_),
_Token('c', SemanticTokenTypes.parameter,
[SemanticTokenModifiers.declaration]),
_Token('c', SemanticTokenTypes.parameter),
_Token('const', SemanticTokenTypes.keyword),
_Token('string1', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token("'test'", SemanticTokenTypes.string),
_Token('const', SemanticTokenTypes.keyword),
_Token('string2', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token(r"'test1 ", SemanticTokenTypes.string),
_Token('string1', SemanticTokenTypes.property),
_Token(' test2 ', SemanticTokenTypes.string),
_Token('foo', SemanticTokenTypes.function),
_Token("'test3'", SemanticTokenTypes.string),
_Token("'", SemanticTokenTypes.string),
_Token('const', SemanticTokenTypes.keyword),
// string3 is raw and should be treated as a single string.
_Token('string3', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token(r"r'$string1 ${string1.length}'", SemanticTokenTypes.string),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
Future<void> test_topLevel() async {
final content = '''
/// strings docs
const strings = <String>["test", 'test', r'test', \'''test\'''];
/// func docs
func(String a) => print(a);
/// abc docs
bool get abc => true;
''';
final expected = [
_Token('/// strings docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('const', SemanticTokenTypes.keyword),
_Token('strings', SemanticTokenTypes.variable,
[SemanticTokenModifiers.declaration]),
_Token('String', SemanticTokenTypes.class_),
_Token('"test"', SemanticTokenTypes.string),
_Token("'test'", SemanticTokenTypes.string),
_Token("r'test'", SemanticTokenTypes.string),
_Token("'''test'''", SemanticTokenTypes.string),
_Token('/// func docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('func', SemanticTokenTypes.function,
[SemanticTokenModifiers.declaration, SemanticTokenModifiers.static]),
_Token('String', SemanticTokenTypes.class_),
_Token('a', SemanticTokenTypes.parameter,
[SemanticTokenModifiers.declaration]),
_Token('print', SemanticTokenTypes.function),
_Token('a', SemanticTokenTypes.parameter),
_Token('/// abc docs', SemanticTokenTypes.comment,
[SemanticTokenModifiers.documentation]),
_Token('bool', SemanticTokenTypes.class_),
_Token('get', SemanticTokenTypes.keyword),
_Token('abc', SemanticTokenTypes.property,
[SemanticTokenModifiers.declaration]),
_Token('true', CustomSemanticTokenTypes.boolean),
];
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final tokens = await getSemanticTokens(mainFileUri);
final decoded = decodeSemanticTokens(content, tokens);
expect(decoded, equals(expected));
}
}
class _Token {
final String content;
final SemanticTokenTypes type;
final List<SemanticTokenModifiers> modifiers;
_Token(this.content, this.type, [this.modifiers = const []]);
@override
int get hashCode => content.hashCode;
@override
bool operator ==(Object o) =>
o is _Token &&
o.content == content &&
o.type == type &&
listEqual(
// Treat nulls the same as empty lists for convenience when comparing.
o.modifiers ?? <SemanticTokenModifiers>[],
modifiers ?? <SemanticTokenModifiers>[],
(SemanticTokenModifiers a, SemanticTokenModifiers b) => a == b);
@override
String toString() => '$content (${[type, ...?modifiers]})';
}