Skip to content

Commit 43f027f

Browse files
committed
Analysis request getReachableSources (#24893).
Implements a new request to get reachable sources. This solves a specific issue for flutter (e.g., inferring execution type by checking the transitive closure of reachabe sources for `dart:flutter`) but can be more generallly useful for other client-side smarts (e.g., "is this a web entry point?" or "is this a test?"). More context here: #24893 [email protected], [email protected] Review URL: https://codereview.chromium.org/1491013002 .
1 parent daf5f6d commit 43f027f

15 files changed

+568
-7
lines changed

pkg/analysis_server/doc/api.html

Lines changed: 50 additions & 2 deletions
Large diffs are not rendered by default.

pkg/analysis_server/lib/plugin/protocol/generated_protocol.dart

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,176 @@ class AnalysisGetHoverResult implements HasToJson {
938938
return JenkinsSmiHash.finish(hash);
939939
}
940940
}
941+
942+
/**
943+
* analysis.getReachableSources params
944+
*
945+
* {
946+
* "file": FilePath
947+
* }
948+
*
949+
* Clients may not extend, implement or mix-in this class.
950+
*/
951+
class AnalysisGetReachableSourcesParams implements HasToJson {
952+
String _file;
953+
954+
/**
955+
* The file for which reachable source information is being requested.
956+
*/
957+
String get file => _file;
958+
959+
/**
960+
* The file for which reachable source information is being requested.
961+
*/
962+
void set file(String value) {
963+
assert(value != null);
964+
this._file = value;
965+
}
966+
967+
AnalysisGetReachableSourcesParams(String file) {
968+
this.file = file;
969+
}
970+
971+
factory AnalysisGetReachableSourcesParams.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object json) {
972+
if (json == null) {
973+
json = {};
974+
}
975+
if (json is Map) {
976+
String file;
977+
if (json.containsKey("file")) {
978+
file = jsonDecoder.decodeString(jsonPath + ".file", json["file"]);
979+
} else {
980+
throw jsonDecoder.missingKey(jsonPath, "file");
981+
}
982+
return new AnalysisGetReachableSourcesParams(file);
983+
} else {
984+
throw jsonDecoder.mismatch(jsonPath, "analysis.getReachableSources params", json);
985+
}
986+
}
987+
988+
factory AnalysisGetReachableSourcesParams.fromRequest(Request request) {
989+
return new AnalysisGetReachableSourcesParams.fromJson(
990+
new RequestDecoder(request), "params", request._params);
991+
}
992+
993+
Map<String, dynamic> toJson() {
994+
Map<String, dynamic> result = {};
995+
result["file"] = file;
996+
return result;
997+
}
998+
999+
Request toRequest(String id) {
1000+
return new Request(id, "analysis.getReachableSources", toJson());
1001+
}
1002+
1003+
@override
1004+
String toString() => JSON.encode(toJson());
1005+
1006+
@override
1007+
bool operator==(other) {
1008+
if (other is AnalysisGetReachableSourcesParams) {
1009+
return file == other.file;
1010+
}
1011+
return false;
1012+
}
1013+
1014+
@override
1015+
int get hashCode {
1016+
int hash = 0;
1017+
hash = JenkinsSmiHash.combine(hash, file.hashCode);
1018+
return JenkinsSmiHash.finish(hash);
1019+
}
1020+
}
1021+
1022+
/**
1023+
* analysis.getReachableSources result
1024+
*
1025+
* {
1026+
* "sources": Map<String, List<String>>
1027+
* }
1028+
*
1029+
* Clients may not extend, implement or mix-in this class.
1030+
*/
1031+
class AnalysisGetReachableSourcesResult implements HasToJson {
1032+
Map<String, List<String>> _sources;
1033+
1034+
/**
1035+
* A mapping from source URIs to directly reachable source URIs. For example,
1036+
* a file "foo.dart" that imports "bar.dart" would have the corresponding
1037+
* mapping { "file:///foo.dart" : ["file:///bar.dart"] }. If "bar.dart" has
1038+
* further imports (or exports) there will be a mapping from the URI
1039+
* "file:///bar.dart" to them. To check if a specific URI is reachable from a
1040+
* given file, clients can check for its presence in the resulting key set.
1041+
*/
1042+
Map<String, List<String>> get sources => _sources;
1043+
1044+
/**
1045+
* A mapping from source URIs to directly reachable source URIs. For example,
1046+
* a file "foo.dart" that imports "bar.dart" would have the corresponding
1047+
* mapping { "file:///foo.dart" : ["file:///bar.dart"] }. If "bar.dart" has
1048+
* further imports (or exports) there will be a mapping from the URI
1049+
* "file:///bar.dart" to them. To check if a specific URI is reachable from a
1050+
* given file, clients can check for its presence in the resulting key set.
1051+
*/
1052+
void set sources(Map<String, List<String>> value) {
1053+
assert(value != null);
1054+
this._sources = value;
1055+
}
1056+
1057+
AnalysisGetReachableSourcesResult(Map<String, List<String>> sources) {
1058+
this.sources = sources;
1059+
}
1060+
1061+
factory AnalysisGetReachableSourcesResult.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object json) {
1062+
if (json == null) {
1063+
json = {};
1064+
}
1065+
if (json is Map) {
1066+
Map<String, List<String>> sources;
1067+
if (json.containsKey("sources")) {
1068+
sources = jsonDecoder.decodeMap(jsonPath + ".sources", json["sources"], valueDecoder: (String jsonPath, Object json) => jsonDecoder.decodeList(jsonPath, json, jsonDecoder.decodeString));
1069+
} else {
1070+
throw jsonDecoder.missingKey(jsonPath, "sources");
1071+
}
1072+
return new AnalysisGetReachableSourcesResult(sources);
1073+
} else {
1074+
throw jsonDecoder.mismatch(jsonPath, "analysis.getReachableSources result", json);
1075+
}
1076+
}
1077+
1078+
factory AnalysisGetReachableSourcesResult.fromResponse(Response response) {
1079+
return new AnalysisGetReachableSourcesResult.fromJson(
1080+
new ResponseDecoder(REQUEST_ID_REFACTORING_KINDS.remove(response.id)), "result", response._result);
1081+
}
1082+
1083+
Map<String, dynamic> toJson() {
1084+
Map<String, dynamic> result = {};
1085+
result["sources"] = sources;
1086+
return result;
1087+
}
1088+
1089+
Response toResponse(String id) {
1090+
return new Response(id, result: toJson());
1091+
}
1092+
1093+
@override
1094+
String toString() => JSON.encode(toJson());
1095+
1096+
@override
1097+
bool operator==(other) {
1098+
if (other is AnalysisGetReachableSourcesResult) {
1099+
return mapEqual(sources, other.sources, (List<String> a, List<String> b) => listEqual(a, b, (String a, String b) => a == b));
1100+
}
1101+
return false;
1102+
}
1103+
1104+
@override
1105+
int get hashCode {
1106+
int hash = 0;
1107+
hash = JenkinsSmiHash.combine(hash, sources.hashCode);
1108+
return JenkinsSmiHash.finish(hash);
1109+
}
1110+
}
9411111
/**
9421112
* analysis.getLibraryDependencies params
9431113
*
@@ -13920,6 +14090,7 @@ class RequestError implements HasToJson {
1392014090
* FORMAT_WITH_ERRORS
1392114091
* GET_ERRORS_INVALID_FILE
1392214092
* GET_NAVIGATION_INVALID_FILE
14093+
* GET_REACHABLE_SOURCES_INVALID_FILE
1392314094
* INVALID_ANALYSIS_ROOT
1392414095
* INVALID_EXECUTION_CONTEXT
1392514096
* INVALID_OVERLAY_CHANGE
@@ -13977,6 +14148,12 @@ class RequestErrorCode implements Enum {
1397714148
*/
1397814149
static const GET_NAVIGATION_INVALID_FILE = const RequestErrorCode._("GET_NAVIGATION_INVALID_FILE");
1397914150

14151+
/**
14152+
* An "analysis.getReachableSources" request specified a FilePath which does
14153+
* not match a file currently subject to analysis.
14154+
*/
14155+
static const GET_REACHABLE_SOURCES_INVALID_FILE = const RequestErrorCode._("GET_REACHABLE_SOURCES_INVALID_FILE");
14156+
1398014157
/**
1398114158
* A path passed as an argument to a request (such as analysis.reanalyze) is
1398214159
* required to be an analysis root, but isn't.
@@ -14083,7 +14260,7 @@ class RequestErrorCode implements Enum {
1408314260
/**
1408414261
* A list containing all of the enum values that are defined.
1408514262
*/
14086-
static const List<RequestErrorCode> VALUES = const <RequestErrorCode>[CONTENT_MODIFIED, FILE_NOT_ANALYZED, FORMAT_INVALID_FILE, FORMAT_WITH_ERRORS, GET_ERRORS_INVALID_FILE, GET_NAVIGATION_INVALID_FILE, INVALID_ANALYSIS_ROOT, INVALID_EXECUTION_CONTEXT, INVALID_OVERLAY_CHANGE, INVALID_PARAMETER, INVALID_REQUEST, NO_INDEX_GENERATED, ORGANIZE_DIRECTIVES_ERROR, REFACTORING_REQUEST_CANCELLED, SERVER_ALREADY_STARTED, SERVER_ERROR, SORT_MEMBERS_INVALID_FILE, SORT_MEMBERS_PARSE_ERRORS, UNANALYZED_PRIORITY_FILES, UNKNOWN_REQUEST, UNKNOWN_SOURCE, UNSUPPORTED_FEATURE];
14263+
static const List<RequestErrorCode> VALUES = const <RequestErrorCode>[CONTENT_MODIFIED, FILE_NOT_ANALYZED, FORMAT_INVALID_FILE, FORMAT_WITH_ERRORS, GET_ERRORS_INVALID_FILE, GET_NAVIGATION_INVALID_FILE, GET_REACHABLE_SOURCES_INVALID_FILE, INVALID_ANALYSIS_ROOT, INVALID_EXECUTION_CONTEXT, INVALID_OVERLAY_CHANGE, INVALID_PARAMETER, INVALID_REQUEST, NO_INDEX_GENERATED, ORGANIZE_DIRECTIVES_ERROR, REFACTORING_REQUEST_CANCELLED, SERVER_ALREADY_STARTED, SERVER_ERROR, SORT_MEMBERS_INVALID_FILE, SORT_MEMBERS_PARSE_ERRORS, UNANALYZED_PRIORITY_FILES, UNKNOWN_REQUEST, UNKNOWN_SOURCE, UNSUPPORTED_FEATURE];
1408714264

1408814265
final String name;
1408914266

@@ -14103,6 +14280,8 @@ class RequestErrorCode implements Enum {
1410314280
return GET_ERRORS_INVALID_FILE;
1410414281
case "GET_NAVIGATION_INVALID_FILE":
1410514282
return GET_NAVIGATION_INVALID_FILE;
14283+
case "GET_REACHABLE_SOURCES_INVALID_FILE":
14284+
return GET_REACHABLE_SOURCES_INVALID_FILE;
1410614285
case "INVALID_ANALYSIS_ROOT":
1410714286
return INVALID_ANALYSIS_ROOT;
1410814287
case "INVALID_EXECUTION_CONTEXT":

pkg/analysis_server/lib/plugin/protocol/protocol.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,16 @@ class Response {
410410
RequestErrorCode.GET_NAVIGATION_INVALID_FILE,
411411
'Error during `analysis.getNavigation`: invalid file.'));
412412

413+
/**
414+
* Initialize a newly created instance to represent the
415+
* GET_REACHABLE_SOURCES_INVALID_FILE error condition.
416+
*/
417+
Response.getReachableSourcesInvalidFile(Request request)
418+
: this(request.id,
419+
error: new RequestError(
420+
RequestErrorCode.GET_REACHABLE_SOURCES_INVALID_FILE,
421+
'Error during `analysis.getReachableSources`: invalid file.'));
422+
413423
/**
414424
* Initialize a newly created instance to represent an error condition caused
415425
* by an analysis.reanalyze [request] that specifies an analysis root that is

pkg/analysis_server/lib/src/constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const String ANALYSIS_GET_HOVER = 'analysis.getHover';
2626
const String ANALYSIS_GET_LIBRARY_DEPENDENCIES =
2727
'analysis.getLibraryDependencies';
2828
const String ANALYSIS_GET_NAVIGATION = 'analysis.getNavigation';
29+
const String ANALYSIS_GET_REACHABLE_SOURCES = 'analysis.getReachableSources';
2930
const String ANALYSIS_REANALYZE = 'analysis.reanalyze';
3031
const String ANALYSIS_SET_ANALYSIS_ROOTS = 'analysis.setAnalysisRoots';
3132
const String ANALYSIS_SET_GENERAL_SUBSCRIPTIONS =

pkg/analysis_server/lib/src/domain_analysis.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:analysis_server/src/operation/operation_analysis.dart'
2121
import 'package:analysis_server/src/protocol/protocol_internal.dart';
2222
import 'package:analysis_server/src/protocol_server.dart';
2323
import 'package:analysis_server/src/services/dependencies/library_dependencies.dart';
24+
import 'package:analysis_server/src/services/dependencies/reachable_source_collector.dart';
2425
import 'package:analyzer/file_system/file_system.dart';
2526
import 'package:analyzer/src/generated/ast.dart';
2627
import 'package:analyzer/src/generated/element.dart';
@@ -164,6 +165,22 @@ class AnalysisDomainHandler implements RequestHandler {
164165
return Response.DELAYED_RESPONSE;
165166
}
166167

168+
/**
169+
* Implement the `analysis.getReachableSources` request.
170+
*/
171+
Response getReachableSources(Request request) {
172+
AnalysisGetReachableSourcesParams params =
173+
new AnalysisGetReachableSourcesParams.fromRequest(request);
174+
ContextSourcePair pair = server.getContextSourcePair(params.file);
175+
if (pair.context == null || pair.source == null) {
176+
return new Response.getReachableSourcesInvalidFile(request);
177+
}
178+
Map<String, List<String>> sources = new ReachableSourceCollector(
179+
pair.source, pair.context).collectSources();
180+
return new AnalysisGetReachableSourcesResult(sources)
181+
.toResponse(request.id);
182+
}
183+
167184
@override
168185
Response handleRequest(Request request) {
169186
try {
@@ -176,6 +193,8 @@ class AnalysisDomainHandler implements RequestHandler {
176193
return getLibraryDependencies(request);
177194
} else if (requestName == ANALYSIS_GET_NAVIGATION) {
178195
return getNavigation(request);
196+
} else if (requestName == ANALYSIS_GET_REACHABLE_SOURCES) {
197+
return getReachableSources(request);
179198
} else if (requestName == ANALYSIS_REANALYZE) {
180199
return reanalyze(request);
181200
} else if (requestName == ANALYSIS_SET_ANALYSIS_ROOTS) {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library services.dependencies.reachable_source_collector;
6+
7+
import 'dart:collection';
8+
9+
import 'package:analyzer/src/generated/engine.dart';
10+
import 'package:analyzer/src/generated/source.dart';
11+
import 'package:analyzer/task/dart.dart';
12+
13+
/// Collects reachable sources.
14+
class ReachableSourceCollector {
15+
final Map<String, List<String>> _sourceMap =
16+
new HashMap<String, List<String>>();
17+
18+
final Source source;
19+
final AnalysisContext context;
20+
ReachableSourceCollector(this.source, this.context) {
21+
if (source == null) {
22+
throw new ArgumentError.notNull('source');
23+
}
24+
if (context == null) {
25+
throw new ArgumentError.notNull('context');
26+
}
27+
}
28+
29+
/// Collect reachable sources.
30+
Map<String, List<String>> collectSources() {
31+
_addDependencies(source);
32+
return _sourceMap;
33+
}
34+
35+
void _addDependencies(Source source) {
36+
37+
String sourceUri = source.uri.toString();
38+
39+
// Careful not to revisit.
40+
if (_sourceMap[source.uri.toString()] != null) {
41+
return;
42+
}
43+
44+
List<Source> sources = <Source>[];
45+
sources.addAll(context.computeResult(source, IMPORTED_LIBRARIES));
46+
sources.addAll(context.computeResult(source, EXPORTED_LIBRARIES));
47+
48+
_sourceMap[sourceUri] =
49+
sources.map((source) => source.uri.toString()).toList();
50+
51+
sources.forEach((s) => _addDependencies(s));
52+
}
53+
}

pkg/analysis_server/test/domain_analysis_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,46 @@ main() {
5555
group('updateContent', testUpdateContent);
5656

5757
group('AnalysisDomainHandler', () {
58+
group('getReachableSources', () {
59+
test('valid sources', () async {
60+
String fileA = '/project/a.dart';
61+
String fileB = '/project/b.dart';
62+
resourceProvider.newFile(fileA, 'import "b.dart";');
63+
resourceProvider.newFile(fileB, '');
64+
65+
server.setAnalysisRoots('0', ['/project/'], [], {});
66+
67+
await server.onAnalysisComplete;
68+
69+
var request =
70+
new AnalysisGetReachableSourcesParams(fileA).toRequest('0');
71+
var response = handler.handleRequest(request);
72+
73+
var json = response.toJson()[Response.RESULT];
74+
75+
// Sanity checks.
76+
expect(json['sources'], hasLength(6));
77+
expect(json['sources']['file:///project/a.dart'],
78+
unorderedEquals(['dart:core', 'file:///project/b.dart']));
79+
expect(json['sources']['file:///project/b.dart'], ['dart:core']);
80+
});
81+
82+
test('invalid source', () async {
83+
resourceProvider.newFile('/project/a.dart', 'import "b.dart";');
84+
server.setAnalysisRoots('0', ['/project/'], [], {});
85+
86+
await server.onAnalysisComplete;
87+
88+
var request =
89+
new AnalysisGetReachableSourcesParams('/does/not/exist.dart')
90+
.toRequest('0');
91+
var response = handler.handleRequest(request);
92+
expect(response.error, isNotNull);
93+
expect(response.error.code,
94+
RequestErrorCode.GET_REACHABLE_SOURCES_INVALID_FILE);
95+
});
96+
});
97+
5898
group('setAnalysisRoots', () {
5999
Response testSetAnalysisRoots(
60100
List<String> included, List<String> excluded) {

0 commit comments

Comments
 (0)