diff --git a/analyzer_plugin/lib/src/angular_driver.dart b/analyzer_plugin/lib/src/angular_driver.dart index e7a14a35..f5c61d3e 100644 --- a/analyzer_plugin/lib/src/angular_driver.dart +++ b/analyzer_plugin/lib/src/angular_driver.dart @@ -45,9 +45,10 @@ class AngularDriver final _addedFiles = new LinkedHashSet(); final _dartFiles = new LinkedHashSet(); final _changedFiles = new LinkedHashSet(); - final _requestedFiles = new HashSet(); + final _requestedDartFiles = new Map>(); + final _requestedHtmlFiles = new Map>(); final _filesToAnalyze = new HashSet(); - final _htmlViewsToAnalyze = new HashSet>(); + final _htmlFilesToAnalyze = new HashSet(); final ByteStore byteStore; FileTracker _fileTracker; final lastSignatures = {}; @@ -66,7 +67,10 @@ class AngularDriver } bool get hasFilesToAnalyze => - _filesToAnalyze.isNotEmpty || _htmlViewsToAnalyze.isNotEmpty; + _filesToAnalyze.isNotEmpty || + _htmlFilesToAnalyze.isNotEmpty || + _requestedDartFiles.isNotEmpty || + _requestedHtmlFiles.isNotEmpty; bool _ownsFile(String path) { return path.endsWith('.dart') || path.endsWith('.html'); @@ -85,15 +89,9 @@ class AngularDriver void fileChanged(String path) { if (_ownsFile(path)) { if (path.endsWith('.html')) { - for (final dartContext - in _fileTracker.getDartPathsReferencingHtml(path)) { - _htmlViewsToAnalyze.add(new Tuple2(path, dartContext)); - } + _htmlFilesToAnalyze.add(path); for (final path in _fileTracker.getHtmlPathsReferencingHtml(path)) { - for (final dartContext - in _fileTracker.getDartPathsReferencingHtml(path)) { - _htmlViewsToAnalyze.add(new Tuple2(path, dartContext)); - } + _htmlFilesToAnalyze.add(path); } for (final path in _fileTracker.getDartPathsAffectedByHtml(path)) { _filesToAnalyze.add(path); @@ -105,19 +103,38 @@ class AngularDriver _scheduler.notify(this); } + Future> requestDartErrors(String path) { + var completer = new Completer>(); + _requestedDartFiles + .putIfAbsent(path, () => >>[]) + .add(completer); + _scheduler.notify(this); + return completer.future; + } + + Future> requestHtmlErrors(String path) { + var completer = new Completer>(); + _requestedHtmlFiles + .putIfAbsent(path, () => >>[]) + .add(completer); + _scheduler.notify(this); + return completer.future; + } + AnalysisDriverPriority get workPriority { if (standardHtml == null) { return AnalysisDriverPriority.interactive; } - - if (_requestedFiles.isNotEmpty) { + if (_requestedDartFiles.isNotEmpty) { + return AnalysisDriverPriority.interactive; + } + if (_requestedHtmlFiles.isNotEmpty) { return AnalysisDriverPriority.interactive; } - // tasks here? if (_filesToAnalyze.isNotEmpty) { return AnalysisDriverPriority.general; } - if (_htmlViewsToAnalyze.isNotEmpty) { + if (_htmlFilesToAnalyze.isNotEmpty) { return AnalysisDriverPriority.general; } if (_changedFiles.isNotEmpty) { @@ -138,16 +155,49 @@ class AngularDriver return; } - if (_requestedFiles.isNotEmpty) { - final path = _requestedFiles.first; - try { - pushDartErrors(path); - pushDartNavigation(path); - pushDartOccurrences(path); - _requestedFiles.remove(path); - } catch (e) { - e; + if (_requestedDartFiles.isNotEmpty) { + final path = _requestedDartFiles.keys.first; + final completers = _requestedDartFiles.remove(path); + // Note: We can't use await here, or the dart analysis becomes a future in + // a queue that won't be completed until the scheduler schedules the dart + // driver, which doesn't happen because its waiting for us. + resolveDart(path, onlyIfChangedSignature: false).then((result) { + completers + .forEach((completer) => completer.complete(result?.errors ?? [])); + }, onError: (e) { + completers.forEach((completer) => completer.completeError(e)); + }); + + return; + } + + if (_requestedHtmlFiles.isNotEmpty) { + final path = _requestedHtmlFiles.keys.first; + final completers = _requestedHtmlFiles.remove(path); + // Note: We can't use await here, or the dart analysis becomes a future in + // a queue that won't be completed until the scheduler schedules the dart + // driver, which doesn't happen because its waiting for us. + // ALSO assume .dart and .html paths correlate, otherwise we'd have to + // wait for all dart analysis to complete. + Future resolvedHtml; + + // Try resolving HTML using the existing dart/html relationships which may + // be already known. However, if we don't see any relationships, try using + // the .dart equivalent. Better than no result -- the real one WILL come. + if (_fileTracker.getDartPathsReferencingHtml(path).isEmpty) { + resolvedHtml = resolveHtmlFrom(path, path.replaceAll(".html", ".dart")); + } else { + resolvedHtml = resolveHtml(path); } + + // After whichever resolution is complete, push errors. + resolvedHtml.then((result) { + completers + .forEach((completer) => completer.complete(result?.errors ?? [])); + }, onError: (e) { + completers.forEach((completer) => completer.completeError(e)); + }); + return; } @@ -158,10 +208,10 @@ class AngularDriver return; } - if (_htmlViewsToAnalyze.isNotEmpty) { - final info = _htmlViewsToAnalyze.first; - pushHtmlErrors(info.item1, info.item2); - _htmlViewsToAnalyze.remove(info); + if (_htmlFilesToAnalyze.isNotEmpty) { + final path = _htmlFilesToAnalyze.first; + pushHtmlErrors(path); + _htmlFilesToAnalyze.remove(path); return; } @@ -221,8 +271,8 @@ class AngularDriver errorCode, error.message, error.correction); } - String getHtmlKey(String htmlPath, String dartPath) { - final key = _fileTracker.getHtmlSignature(htmlPath, dartPath); + String getHtmlKey(String htmlPath) { + final key = _fileTracker.getHtmlSignature(htmlPath); return key.toHex() + '.ngresolved'; } @@ -239,8 +289,8 @@ class AngularDriver source.exists() ? source.contents.data : "")(getSource(path)); } - Future resolveHtml(String htmlPath, String dartPath) async { - final key = getHtmlKey(htmlPath, dartPath); + Future resolveHtml(String htmlPath) async { + final key = getHtmlKey(htmlPath); final htmlSource = _sourceFactory.forUri("file:" + htmlPath); final List bytes = byteStore.get(key); if (bytes != null) { @@ -251,9 +301,36 @@ class AngularDriver return new DirectivesResult([], errors); } + final result = new DirectivesResult([], []); + + for (final dartContext + in _fileTracker.getDartPathsReferencingHtml(htmlPath)) { + final pairResult = await resolveHtmlFrom(htmlPath, dartContext); + result.directives.addAll(pairResult.directives); + result.errors.addAll(pairResult.errors); + } + + final summary = new LinkedHtmlSummaryBuilder() + ..errors = summarizeErrors( + result.errors.where((error) => error is! FromFilePrefixedError)) + ..errorsFromPath = result.errors + .where((error) => error is FromFilePrefixedError) + .map((error) => new SummarizedAnalysisErrorFromPathBuilder() + ..path = (error as FromFilePrefixedError).fromSourcePath + ..originalError = + summarizeError((error as FromFilePrefixedError).originalError)); + final List newBytes = summary.toBuffer(); + byteStore.put(key, newBytes); + + return result; + } + + Future resolveHtmlFrom( + String htmlPath, String dartPath) async { final result = await getDirectives(dartPath); final directives = result.directives; final unit = (await dartDriver.getUnitElement(dartPath)).element; + final htmlSource = _sourceFactory.forUri("file:" + htmlPath); if (unit == null) return null; final context = unit.context; @@ -318,17 +395,6 @@ class AngularDriver } } - final summary = new LinkedHtmlSummaryBuilder() - ..errors = summarizeErrors( - errors.where((error) => error is! FromFilePrefixedError)) - ..errorsFromPath = errors - .where((error) => error is FromFilePrefixedError) - .map((error) => new SummarizedAnalysisErrorFromPathBuilder() - ..path = (error as FromFilePrefixedError).fromSourcePath - ..originalError = - summarizeError((error as FromFilePrefixedError).originalError)); - final List newBytes = summary.toBuffer(); - byteStore.put(key, newBytes); return new DirectivesResult(directives, errors); } @@ -364,8 +430,8 @@ class AngularDriver return contents; } - Future pushHtmlErrors(String htmlPath, String dartPath) async { - final errors = (await resolveHtml(htmlPath, dartPath)).errors; + Future pushHtmlErrors(String htmlPath) async { + final errors = (await resolveHtml(htmlPath)).errors; final lineInfo = new LineInfo.fromContent(getFileContent(htmlPath)); final serverErrors = protocol.doAnalysisError_listFromEngine( dartDriver.analysisOptions, lineInfo, errors); @@ -389,7 +455,7 @@ class AngularDriver } Future resolveDart(String path, - {bool withDirectives: false}) async { + {bool withDirectives: false, bool onlyIfChangedSignature: true}) async { final baseKey = await dartDriver.getUnitElementSignature(path); // This happens when the path is..."hidden by a generated file"..whch I @@ -402,7 +468,7 @@ class AngularDriver final key = baseKey + '.ngresolved'; - if (lastSignatures[path] == key) { + if (lastSignatures[path] == key && onlyIfChangedSignature) { return null; } @@ -413,8 +479,8 @@ class AngularDriver if (bytes != null) { final summary = new LinkedDartSummary.fromBuffer(bytes); - for (final htmlView in summary.referencedHtmlFiles) { - _htmlViewsToAnalyze.add(new Tuple2(htmlView, path)); + for (final htmlPath in summary.referencedHtmlFiles) { + _htmlFilesToAnalyze.add(htmlPath); } _fileTracker.setDartHasTemplate(path, summary.hasDartTemplates); @@ -480,8 +546,7 @@ class AngularDriver errors.addAll(tplErrorListener.errors.where( (e) => !view.template.ignoredErrors.contains(e.errorCode.name))); } else if (view?.templateUriSource != null) { - _htmlViewsToAnalyze - .add(new Tuple2(view.templateUriSource.fullName, path)); + _htmlFilesToAnalyze.add(view.templateUriSource.fullName); htmlViews.add(view.templateUriSource.fullName); } diff --git a/analyzer_plugin/lib/src/file_tracker.dart b/analyzer_plugin/lib/src/file_tracker.dart index 0f069756..85a6b899 100644 --- a/analyzer_plugin/lib/src/file_tracker.dart +++ b/analyzer_plugin/lib/src/file_tracker.dart @@ -83,12 +83,15 @@ class FileTracker { return signature; } - ApiSignature getHtmlSignature(String htmlPath, String dartPath) { + ApiSignature getHtmlSignature(String htmlPath) { final signature = new ApiSignature(); signature.addBytes(_fileHasher.getContentHash(htmlPath).toByteList()); - signature.addBytes(_fileHasher.getUnitElementHash(dartPath).toByteList()); - for (final subHtmlPath in getHtmlPathsAffectingDartContext(dartPath)) { - signature.addBytes(_fileHasher.getContentHash(subHtmlPath).toByteList()); + for (final dartPath in getDartPathsReferencingHtml(htmlPath)) { + signature.addBytes(_fileHasher.getUnitElementHash(dartPath).toByteList()); + for (final subHtmlPath in getHtmlPathsAffectingDartContext(dartPath)) { + signature + .addBytes(_fileHasher.getContentHash(subHtmlPath).toByteList()); + } } return signature; } diff --git a/analyzer_plugin/lib/starter.dart b/analyzer_plugin/lib/starter.dart new file mode 100644 index 00000000..d39e131e --- /dev/null +++ b/analyzer_plugin/lib/starter.dart @@ -0,0 +1,69 @@ +import 'package:analysis_server/src/analysis_server.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:angular_analyzer_plugin/src/angular_driver.dart'; +import 'package:analyzer/src/context/builder.dart'; + +class Starter { + final angularDrivers = {}; + AnalysisServer server; + + void start(AnalysisServer server) { + this.server = server; + ContextBuilder.onCreateAnalysisDriver = onCreateAnalysisDriver; + server.onResultErrorSupplementor = sumErrors; + server.onNoAnalysisResult = sendHtmlResult; + } + + void onCreateAnalysisDriver( + analysisDriver, + scheduler, + logger, + resourceProvider, + byteStore, + contentOverlay, + driverPath, + sourceFactory, + analysisOptions) { + final AngularDriver driver = new AngularDriver(server, analysisDriver, + scheduler, byteStore, sourceFactory, contentOverlay); + angularDrivers[driverPath] = driver; + server.onFileAdded.listen((String path) { + if (server.contextManager.getContextFolderFor(path).path == driverPath) { + // only the owning driver "adds" the path + driver.addFile(path); + } else { + // but the addition of a file is a "change" to all the other drivers + driver.fileChanged(path); + } + }); + server.onFileChanged.listen((String path) { + // all drivers get change notification + driver.fileChanged(path); + }); + } + + Future sumErrors(String path, List errors) async { + for (final driver in angularDrivers.values) { + final angularErrors = await driver.requestDartErrors(path); + errors.addAll(angularErrors); + } + return null; + } + + Future sendHtmlResult(String path, Function sendFn) async { + for (final driverPath in angularDrivers.keys) { + if (server.contextManager.getContextFolderFor(path).path == driverPath) { + final driver = angularDrivers[driverPath]; + // only the owning driver "adds" the path + final angularErrors = await driver.requestHtmlErrors(path); + sendFn( + driver.dartDriver.analysisOptions, + new LineInfo.fromContent(driver.getFileContent(path)), + angularErrors); + return; + } + } + + send(null, null, null); + } +} diff --git a/analyzer_plugin/test/angular_driver_test.dart b/analyzer_plugin/test/angular_driver_test.dart index 4216d8d5..94485c6b 100644 --- a/analyzer_plugin/test/angular_driver_test.dart +++ b/analyzer_plugin/test/angular_driver_test.dart @@ -2328,31 +2328,30 @@ class ChildComponent {} @reflectiveTest class ResolveHtmlTemplatesTest extends AbstractAngularTest { List