| // Copyright (c) 2018, 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/src/computer/computer_folding.dart'; |
| import 'package:analysis_server/src/protocol_server.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../../abstract_context.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(FoldingComputerTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class FoldingComputerTest extends AbstractContextTest { |
| static const commentKinds = { |
| FoldingKind.FILE_HEADER, |
| FoldingKind.COMMENT, |
| FoldingKind.DOCUMENTATION_COMMENT |
| }; |
| |
| late String sourcePath; |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| sourcePath = convertPath('$testPackageLibPath/test.dart'); |
| } |
| |
| Future<void> test_annotations() async { |
| var content = ''' |
| @myMultilineAnnotation/*1:INC*/( |
| "this", |
| "is a test" |
| )/*1:EXC:ANNOTATIONS*/ |
| void f() {} |
| |
| @noFoldNecessary |
| main2() {} |
| |
| @multipleAnnotations1/*2:INC*/( |
| "this", |
| "is a test" |
| ) |
| @multipleAnnotations2() |
| @multipleAnnotations3/*2:EXC:ANNOTATIONS*/ |
| main3() {} |
| |
| @noFoldsForSingleClassAnnotation |
| class MyClass {} |
| |
| @folded.classAnnotation1/*3:INC*/() |
| @foldedClassAnnotation2/*3:EXC:ANNOTATIONS*/ |
| class MyClass2 {/*4:INC*/ |
| @fieldAnnotation1/*5:INC*/ |
| @fieldAnnotation2/*5:EXC:ANNOTATIONS*/ |
| int myField; |
| |
| @getterAnnotation1/*6:INC*/ |
| @getterAnnotation2/*6:EXC:ANNOTATIONS*/ |
| int get myThing => 1; |
| |
| @setterAnnotation1/*7:INC*/ |
| @setterAnnotation2/*7:EXC:ANNOTATIONS*/ |
| void set myThing(int value) {} |
| |
| @methodAnnotation1/*8:INC*/ |
| @methodAnnotation2/*8:EXC:ANNOTATIONS*/ |
| void myMethod() {} |
| |
| @constructorAnnotation1/*9:INC*/ |
| @constructorAnnotation1/*9:EXC:ANNOTATIONS*/ |
| MyClass2() {} |
| /*4:INC:CLASS_BODY*/} |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_assertInitializer() async { |
| var content = ''' |
| class C {/*1:INC*/ |
| C() : assert(/*2:INC*/ |
| true, |
| '' |
| /*2:INC:INVOCATION*/); |
| /*1:INC:CLASS_BODY*/} |
| '''; |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_assertStatement() async { |
| var content = ''' |
| void f() {/*1:INC*/ |
| assert(/*2:INC*/ |
| true, |
| '' |
| /*2:INC:INVOCATION*/); |
| /*1:INC:FUNCTION_BODY*/} |
| '''; |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_class() async { |
| var content = ''' |
| // Content before |
| |
| class Person {/*1:INC*/ |
| Person() {/*2:INC*/ |
| print("Hello, world!"); |
| /*2:INC:FUNCTION_BODY*/} |
| |
| void sayHello() {/*3:INC*/ |
| print("Hello, world!"); |
| /*3:INC:FUNCTION_BODY*/} |
| /*1:INC:CLASS_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_comment_is_not_considered_file_header() async { |
| var content = """ |
| // This is not the file header/*1:EXC*/ |
| // It's just a comment/*1:INC:COMMENT*/ |
| void f() {} |
| """; |
| |
| // Since there are no region comment markers above |
| // just check the length instead of the contents |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_comment_multiline() async { |
| var content = ''' |
| void f() { |
| /*/*1:EXC*/ |
| * comment 1 |
| *//*1:EXC:COMMENT*/ |
| |
| /* this comment starts on the same line as delimeters/*2:EXC*/ |
| * second line |
| *//*2:EXC:COMMENT*/ |
| } |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, commentKinds); |
| } |
| |
| Future<void> test_comment_singleFollowedByBlankLine() async { |
| var content = ''' |
| void f() { |
| // this is/*1:EXC*/ |
| // a comment/*1:INC:COMMENT*/ |
| /// this is not part of it |
| } |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, commentKinds); |
| } |
| |
| Future<void> test_comment_singleFollowedByMulti() async { |
| var content = ''' |
| void f() { |
| // this is/*1:EXC*/ |
| // a comment/*1:INC:COMMENT*/ |
| /* this is not part of it */ |
| String foo; |
| } |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, commentKinds); |
| } |
| |
| Future<void> test_comment_singleFollowedByTripleSlash() async { |
| var content = ''' |
| void f() { |
| // this is/*1:EXC*/ |
| // a comment/*1:INC:COMMENT*/ |
| /// this is not part of it |
| } |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, commentKinds); |
| } |
| |
| Future<void> test_constructor_invocations() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| return new Text(/*2:INC*/ |
| "Hello, world!", |
| /*2:INC:INVOCATION*/); |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_file_header() async { |
| var content = """ |
| // Copyright some year by some people/*1:EXC*/ |
| // See LICENCE etc./*1:INC:FILE_HEADER*/ |
| |
| // This is not the file header |
| // It's just a comment |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, {FoldingKind.FILE_HEADER}); |
| } |
| |
| Future<void> test_file_header_does_not_include_block_comments() async { |
| var content = """ |
| /* |
| * Copyright some year by some people |
| * See LICENCE etc. |
| */ |
| /* This shouldn't be part of the file header */ |
| |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, {FoldingKind.FILE_HEADER}); |
| } |
| |
| Future<void> test_file_header_with_no_function_comment() async { |
| var content = ''' |
| // Copyright some year by some people/*1:EXC*/ |
| // See LICENCE etc./*1:INC:FILE_HEADER*/ |
| |
| void f() {} |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, {FoldingKind.FILE_HEADER}); |
| } |
| |
| Future<void> test_file_header_with_non_end_of_line_comment() async { |
| var content = """ |
| // Copyright some year by some people/*1:EXC*/ |
| // See LICENCE etc./*1:INC:FILE_HEADER*/ |
| /* This shouldn't be part of the file header */ |
| |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, {FoldingKind.FILE_HEADER}); |
| } |
| |
| Future<void> test_file_header_with_script_prefix() async { |
| var content = """ |
| #! /usr/bin/dart |
| // Copyright some year by some people/*1:EXC*/ |
| // See LICENCE etc./*1:INC:FILE_HEADER*/ |
| |
| // This is not the file header |
| // It's just a comment |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content, {FoldingKind.FILE_HEADER}); |
| } |
| |
| Future<void> test_fileHeader_singleFollowedByBlank() async { |
| var content = ''' |
| // this is/*1:EXC*/ |
| // a file header/*1:INC:FILE_HEADER*/ |
| |
| // this is not part of it |
| void f() {} |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_function() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| print("Hello, world!"); |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_function_expression_invocation() async { |
| var content = ''' |
| // Content before |
| |
| getFunc() => (String a, String b) {/*1:INC*/ |
| print(a); |
| /*1:INC:FUNCTION_BODY*/}; |
| |
| main2() {/*2:INC*/ |
| getFunc()(/*3:INC*/ |
| "one", |
| "two" |
| /*3:INC:INVOCATION*/); |
| /*2:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_function_with_dart_doc() async { |
| var content = ''' |
| // Content before |
| |
| /// This is a doc comment/*1:EXC*/ |
| /// that spans lines/*1:INC:DOCUMENTATION_COMMENT*/ |
| void f() {/*2:INC*/ |
| print("Hello, world!"); |
| /*2:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_invocations() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| print(/*2:INC*/ |
| "Hello, world!", |
| /*2:INC:INVOCATION*/); |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_literal_list() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| final List<String> things = <String>[/*2:INC*/ |
| "one", |
| "two" |
| /*2:INC:LITERAL*/]; |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_literal_map() async { |
| var content = ''' |
| // Content before |
| |
| main2() {/*1:INC*/ |
| final Map<String, String> things = <String, String>{/*2:INC*/ |
| "one": "one", |
| "two": "two" |
| /*2:INC:LITERAL*/}; |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_mixin() async { |
| var content = ''' |
| // Content before |
| |
| mixin M {/*1:INC*/ |
| void m() {/*3:INC*/ |
| print("Got to m"); |
| /*3:INC:FUNCTION_BODY*/} |
| /*1:INC:CLASS_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_multiple_directive_types() async { |
| var content = """ |
| import/*1:INC*/ 'dart:async'; |
| |
| // We can have comments |
| import 'package:a/b.dart'; |
| import 'package:b/c.dart'; |
| |
| export '../a.dart';/*1:EXC:DIRECTIVES*/ |
| |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_multiple_import_directives() async { |
| var content = """ |
| import/*1:INC*/ 'dart:async'; |
| |
| // We can have comments |
| import 'package:a/b.dart'; |
| import 'package:b/c.dart'; |
| |
| import '../a.dart';/*1:EXC:DIRECTIVES*/ |
| |
| void f() {} |
| """; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_nested_function() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| doPrint() {/*2:INC*/ |
| print("Hello, world!"); |
| /*2:INC:FUNCTION_BODY*/} |
| doPrint(); |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_nested_invocations() async { |
| var content = ''' |
| // Content before |
| |
| void f() {/*1:INC*/ |
| a(/*2:INC*/ |
| b(/*3:INC*/ |
| c(/*4:INC*/ |
| d() |
| /*4:INC:INVOCATION*/), |
| /*3:INC:INVOCATION*/), |
| /*2:INC:INVOCATION*/); |
| /*1:INC:FUNCTION_BODY*/} |
| |
| // Content after |
| '''; |
| |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_parameters_function() async { |
| var content = ''' |
| foo(/*1:INC*/ |
| String aaaaa, |
| String bbbbb, { |
| String ccccc, |
| }/*1:INC:PARAMETERS*/) {} |
| '''; |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_parameters_method() async { |
| var content = ''' |
| class C {/*1:INC*/ |
| C(/*2:INC*/ |
| String aaaaa, |
| String bbbbb, |
| /*2:INC:PARAMETERS*/) : super(); |
| /*1:INC:CLASS_BODY*/} |
| '''; |
| final regions = await _computeRegions(content); |
| _compareRegions(regions, content); |
| } |
| |
| Future<void> test_single_import_directives() async { |
| var content = """ |
| import 'dart:async'; |
| |
| void f() {} |
| """; |
| |
| // Since there are no region comment markers above |
| // just check the length instead of the contents |
| final regions = await _computeRegions(content); |
| expect(regions, hasLength(0)); |
| } |
| |
| /// Compares provided folding regions with expected |
| /// regions extracted from the comments in the provided content. |
| /// |
| /// If [onlyKinds] is supplied only regions of that type will be compared. |
| void _compareRegions(List<FoldingRegion> regions, String content, |
| [Set<FoldingKind>? onlyKinds]) { |
| // Find all numeric markers for region starts. |
| final regex = RegExp(r'/\*(\d+):(INC|EXC)\*/'); |
| final expectedRegions = regex.allMatches(content); |
| |
| if (onlyKinds != null) { |
| regions = |
| regions.where((region) => onlyKinds.contains(region.kind)).toList(); |
| } |
| |
| // Check we didn't get more than expected, since the loop below only |
| // checks for the presence of matches, not absence. |
| expect(regions, hasLength(expectedRegions.length)); |
| |
| // Go through each marker, find the expected region start/end and |
| // ensure it's in the results. |
| for (var m in expectedRegions) { |
| final i = m.group(1); |
| final inclusiveStart = m.group(2) == 'INC'; |
| // Find the end marker. |
| final endMatch = |
| RegExp('/\\*$i:(INC|EXC):(.+?)\\*/').firstMatch(content)!; |
| |
| final inclusiveEnd = endMatch.group(1) == 'INC'; |
| final expectedKindString = endMatch.group(2); |
| final expectedKind = FoldingKind.VALUES.firstWhere( |
| (f) => f.toString() == 'FoldingKind.$expectedKindString', |
| orElse: () => throw Exception( |
| 'Annotated test code references $expectedKindString but ' |
| 'this does not exist in FoldingKind')); |
| |
| final expectedStart = inclusiveStart ? m.start : m.end; |
| final expectedLength = |
| (inclusiveEnd ? endMatch.end : endMatch.start) - expectedStart; |
| |
| expect(regions, |
| contains(FoldingRegion(expectedKind, expectedStart, expectedLength))); |
| } |
| } |
| |
| Future<List<FoldingRegion>> _computeRegions(String sourceContent) async { |
| newFile(sourcePath, sourceContent); |
| var result = |
| await (await session).getResolvedUnit(sourcePath) as ResolvedUnitResult; |
| var computer = DartUnitFoldingComputer(result.lineInfo, result.unit); |
| return computer.compute(); |
| } |
| } |