| // Copyright (c) 2026, 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 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import '../lsp_server/integration_tests.dart'; | |
| /// A mixin that can create simple plugins that use | |
| /// 'package:analysis_server_plugin' for use in integration tests. | |
| mixin PluginMixin on AbstractLspAnalysisServerIntegrationTest { | |
| late var pluginsFolderPath = Directory.systemTemp | |
| .createTempSync('analysisServer_test_integration_plugins') | |
| .resolveSymbolicLinksSync(); | |
| /// A simple plugin that always produces a diagnostic at 0:0 for every file | |
| /// that is analyzed. | |
| late final alwaysProduceDiagnosticPlugin = TestPlugin( | |
| 'always_produce_diagnostic', | |
| _createPluginSourceCode( | |
| className: 'AlwaysProduceDiagnostic', | |
| ruleBody: 'rule.reportAtOffset(0, 0);', | |
| ), | |
| ); | |
| /// A simple plugin that throws in its constructor. | |
| late final throwInConstructorPlugin = TestPlugin( | |
| 'throw_in_constructor', | |
| _createPluginSourceCode( | |
| className: 'ThrowInConstructor', | |
| ruleBody: '', | |
| constructorBody: 'throw UnimplementedError();', | |
| ), | |
| ); | |
| /// A simple plugin that throws an exception asynchronously. | |
| late final throwAsynchronouslyPlugin = TestPlugin( | |
| 'throw_async', | |
| _createPluginSourceCode( | |
| className: 'ThrowAsync', | |
| ruleBody: 'Future.value().then(() => throw UnimplementedError());', | |
| ), | |
| ); | |
| /// A simple plugin that throws during analysis. | |
| late final throwDuringAnalysisPlugin = TestPlugin( | |
| 'throw_during_analysis', | |
| _createPluginSourceCode( | |
| className: 'ThrowDuringAnalysis', | |
| ruleBody: 'throw UnimplementedError();', | |
| ), | |
| ); | |
| void createPlugins(List<TestPlugin> plugins) { | |
| var analysisOptionsContent = StringBuffer()..writeln('plugins:'); | |
| for (var plugin in plugins) { | |
| var name = plugin.name; | |
| var pluginPath = _createPlugin(plugin); | |
| analysisOptionsContent | |
| ..writeln(' $name:') | |
| ..writeln(' path: ${jsonEncode(pluginPath)}'); | |
| } | |
| // Override paths to the plugin packages to use them from the SDK instead | |
| // of trying to pull them from Pub when the plugins are started. | |
| // `PluginPackageGenerator.generatePubspec` may contain a version matching | |
| // the local code that has not been pushed to Pub. | |
| var packages = [ | |
| '_fe_analyzer_shared', | |
| 'analysis_server_plugin', | |
| 'analyzer_plugin', | |
| 'analyzer', | |
| ]; | |
| analysisOptionsContent.writeln(' dependency_overrides:'); | |
| for (var package in packages) { | |
| var packagePath = _getSdkSourcePackagePath(package); | |
| analysisOptionsContent.writeln(''' | |
| $package: | |
| path: ${jsonEncode(packagePath)} | |
| '''); | |
| } | |
| newFile(analysisOptionsPath, analysisOptionsContent.toString()); | |
| } | |
| @override | |
| Future<void> tearDown() async { | |
| await super.tearDown(); | |
| Directory(pluginsFolderPath).deleteSync(recursive: true); | |
| } | |
| /// Creates a plugin in the temporary directory and returns its path. | |
| String _createPlugin(TestPlugin plugin) { | |
| var name = plugin.name; | |
| var sourceCode = plugin.sourceCode; | |
| var pluginPath = pathContext.join(pluginsFolderPath, name); | |
| var libFolder = pathContext.join(pluginPath, 'lib'); | |
| newFolder(pluginPath); | |
| newFolder(libFolder); | |
| newFile(pathContext.join(libFolder, 'main.dart'), sourceCode); | |
| newFile(pathContext.join(pluginPath, 'pubspec.yaml'), ''' | |
| name: $name | |
| description: A simple plugin. | |
| version: 1.0.0 | |
| environment: | |
| sdk: ^3.12.0 | |
| '''); | |
| return pluginPath; | |
| } | |
| /// Creates the source for a simple plugin. | |
| String _createPluginSourceCode({ | |
| required String className, | |
| String code = 'my_diagnostic', | |
| String diagnosticMessage = 'A diagnostic generated by a plugin.', | |
| String ruleName = 'my_diagnostic_rule', | |
| String ruleDescription = 'A diagnostic rule', | |
| required String ruleBody, | |
| String constructorBody = '', | |
| }) { | |
| return ''' | |
| import 'package:analysis_server_plugin/plugin.dart'; | |
| import 'package:analysis_server_plugin/registry.dart'; | |
| import 'package:analyzer/analysis_rule/analysis_rule.dart'; | |
| import 'package:analyzer/analysis_rule/rule_context.dart'; | |
| import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:analyzer/dart/ast/visitor.dart'; | |
| import 'package:analyzer/error/error.dart'; | |
| final plugin = ${className}Plugin(); | |
| class ${className}Plugin extends Plugin { | |
| @override | |
| String get name => 'plugin: $className'; | |
| ${className}Plugin() { | |
| $constructorBody | |
| } | |
| @override | |
| void register(PluginRegistry registry) { | |
| registry.registerWarningRule(${className}Rule()); | |
| } | |
| } | |
| class ${className}Rule extends AnalysisRule { | |
| static const LintCode code = LintCode('$code', ${jsonEncode(diagnosticMessage)}); | |
| ${className}Rule() | |
| : super( | |
| name: '$ruleName', | |
| description: ${jsonEncode(ruleDescription)}, | |
| ); | |
| @override | |
| LintCode get diagnosticCode => code; | |
| @override | |
| void registerNodeProcessors( | |
| RuleVisitorRegistry registry, | |
| RuleContext context, | |
| ) { | |
| var visitor = _${className}Visitor(this); | |
| registry.addCompilationUnit(this, visitor); | |
| } | |
| } | |
| class _${className}Visitor extends SimpleAstVisitor<void> { | |
| final AnalysisRule rule; | |
| _${className}Visitor(this.rule); | |
| @override | |
| void visitCompilationUnit(CompilationUnit node) { | |
| $ruleBody | |
| } | |
| } | |
| '''; | |
| } | |
| /// Gets the source path to [package] in the local SDK checkout. | |
| /// | |
| /// This is used for dependency overrides for plugins so that they run using | |
| /// the local source code and not pulled from Pub. | |
| String _getSdkSourcePackagePath(String package) { | |
| var libFolderUri = Isolate.resolvePackageUriSync( | |
| Uri.parse('package:$package/'), | |
| ); | |
| var rootFolderPath = pathContext.normalize( | |
| pathContext.join(libFolderUri!.toFilePath(), '..'), | |
| ); | |
| return rootFolderPath; | |
| } | |
| } | |
| class TestPlugin { | |
| final String name; | |
| final String sourceCode; | |
| new(this.name, this.sourceCode); | |
| } |