Merge pull request #1753 from dart-lang/flutter_html
flutter_html initial impl
diff --git a/example/all.yaml b/example/all.yaml
index 196bc7b..c8452a5 100644
--- a/example/all.yaml
+++ b/example/all.yaml
@@ -41,6 +41,7 @@
- avoid_types_on_closure_parameters
- avoid_unused_constructor_parameters
- avoid_void_async
+ - avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_extensions
- camel_case_types
diff --git a/lib/src/rules.dart b/lib/src/rules.dart
index e4dab7d..f3f58ee 100644
--- a/lib/src/rules.dart
+++ b/lib/src/rules.dart
@@ -42,6 +42,7 @@
import 'package:linter/src/rules/avoid_types_on_closure_parameters.dart';
import 'package:linter/src/rules/avoid_unused_constructor_parameters.dart';
import 'package:linter/src/rules/avoid_void_async.dart';
+import 'package:linter/src/rules/avoid_web_libraries_in_flutter.dart';
import 'package:linter/src/rules/await_only_futures.dart';
import 'package:linter/src/rules/camel_case_extensions.dart';
import 'package:linter/src/rules/camel_case_types.dart';
@@ -198,6 +199,7 @@
..register(AvoidTypesOnClosureParameters())
..register(AvoidUnusedConstructorParameters())
..register(AvoidVoidAsync())
+ ..register(AvoidWebLibrariesInFlutter())
..register(AwaitOnlyFutures())
..register(CamelCaseExtensions())
..register(CamelCaseTypes())
diff --git a/lib/src/rules/avoid_web_libraries_in_flutter.dart b/lib/src/rules/avoid_web_libraries_in_flutter.dart
new file mode 100644
index 0000000..ef72a16
--- /dev/null
+++ b/lib/src/rules/avoid_web_libraries_in_flutter.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2019, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:linter/src/analyzer.dart';
+import 'package:linter/src/ast.dart';
+import 'package:yaml/yaml.dart';
+
+const _desc = r'Avoid using web-only libraries outside Flutter web projects.';
+
+const _details = r'''Avoid using web libraries, `dart:html`, `dart:js` and
+`dart:js_util` in non-web Flutter projects. These libraries are not supported
+outside a web context and functionality that depends on them will fail at
+runtime.
+
+Web library access is allowed in:
+
+* projects meant to run on the web (e.g., have a `web/` directory)
+* plugin packages that declare `web` as a supported context
+
+otherwise, imports of `dart:html`, `dart:js` and `dart:js_util` are flagged.
+''';
+
+/// todo (pq): consider making a utility and sharing w/ `prefer_relative_imports`
+YamlMap _parseYaml(String content) {
+ try {
+ final doc = loadYamlNode(content);
+ if (doc is YamlMap) {
+ return doc;
+ }
+ // ignore: avoid_catches_without_on_clauses
+ } catch (_) {
+ // Fall-through.
+ }
+ return YamlMap();
+}
+
+class AvoidWebLibrariesInFlutter extends LintRule implements NodeLintRule {
+ AvoidWebLibrariesInFlutter()
+ : super(
+ name: 'avoid_web_libraries_in_flutter',
+ description: _desc,
+ details: _details,
+ maturity: Maturity.experimental,
+ group: Group.errors);
+
+ @override
+ void registerNodeProcessors(
+ NodeLintRegistry registry, LinterContext context) {
+ final visitor = _Visitor(this);
+ registry.addCompilationUnit(this, visitor);
+ registry.addImportDirective(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ File pubspecFile;
+
+ final rule;
+ bool _shouldValidateUri;
+
+ _Visitor(this.rule);
+
+ bool get shouldValidateUri => _shouldValidateUri ??= checkForValidation();
+
+ bool checkForValidation() {
+ if (pubspecFile == null) {
+ return false;
+ }
+
+ var parsedPubspec;
+ try {
+ final content = pubspecFile.readAsStringSync();
+ parsedPubspec = _parseYaml(content);
+ // ignore: avoid_catches_without_on_clauses
+ } catch (_) {
+ return false;
+ }
+
+ // Check for Flutter.
+ if ((parsedPubspec['dependencies'] ?? const {})['flutter'] == null) {
+ return false;
+ }
+
+ // Check for a web directory or a web plugin context declaration.
+ return !pubspecFile.parent.getChild('web').exists &&
+ ((parsedPubspec['flutter'] ?? const {})['plugin'] ?? const {})['web'] ==
+ null;
+ }
+
+ bool isWebUri(String uri) {
+ final uriLength = uri.length;
+ return (uriLength == 9 && uri == 'dart:html') ||
+ (uriLength == 7 && uri == 'dart:js') ||
+ (uriLength == 12 && uri == 'dart:js_util');
+ }
+
+ @override
+ void visitCompilationUnit(CompilationUnit node) {
+ pubspecFile = locatePubspecFile(node);
+ }
+
+ @override
+ void visitImportDirective(ImportDirective node) {
+ if (isWebUri(node.uri.stringValue) && shouldValidateUri) {
+ rule.reportLint(node);
+ }
+ }
+}
diff --git a/test/_data/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart b/test/_data/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart
new file mode 100644
index 0000000..21576d3
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/no_pubspec/lib/main.dart
@@ -0,0 +1,3 @@
+import 'dart:html'; //OK
+import 'dart:js'; //OK
+import 'dart:js_util'; //OK
diff --git a/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart b/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart
new file mode 100644
index 0000000..21576d3
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/lib/main.dart
@@ -0,0 +1,3 @@
+import 'dart:html'; //OK
+import 'dart:js'; //OK
+import 'dart:js_util'; //OK
diff --git a/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml b/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml
new file mode 100644
index 0000000..6666d30
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/non_flutter_app/pubspec.yaml
@@ -0,0 +1,5 @@
+name: non_flutter_app
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
diff --git a/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart b/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart
new file mode 100644
index 0000000..4e68cc2
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/main.dart
@@ -0,0 +1,3 @@
+import 'dart:html'; //LINT
+import 'dart:js'; //LINT
+import 'dart:js_util'; //LINT
diff --git a/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart b/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart
new file mode 100644
index 0000000..fa12ba8
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/non_web_app/lib/second.dart
@@ -0,0 +1 @@
+import 'dart:math'; //OK
diff --git a/test/_data/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml b/test/_data/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml
new file mode 100644
index 0000000..2d63f91
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/non_web_app/pubspec.yaml
@@ -0,0 +1,14 @@
+name: non_web_app
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ cupertino_icons: ^0.1.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
diff --git a/test/_data/avoid_web_libraries_in_flutter/web_app/lib/main.dart b/test/_data/avoid_web_libraries_in_flutter/web_app/lib/main.dart
new file mode 100644
index 0000000..21576d3
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/web_app/lib/main.dart
@@ -0,0 +1,3 @@
+import 'dart:html'; //OK
+import 'dart:js'; //OK
+import 'dart:js_util'; //OK
diff --git a/test/_data/avoid_web_libraries_in_flutter/web_app/pubspec.yaml b/test/_data/avoid_web_libraries_in_flutter/web_app/pubspec.yaml
new file mode 100644
index 0000000..fe0dbb9
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/web_app/pubspec.yaml
@@ -0,0 +1,17 @@
+name: sample_project
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ cupertino_icons: ^0.1.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/test/_data/avoid_web_libraries_in_flutter/web_app/web/README b/test/_data/avoid_web_libraries_in_flutter/web_app/web/README
new file mode 100644
index 0000000..b36eb37
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/web_app/web/README
@@ -0,0 +1 @@
+placeholder.
diff --git a/test/_data/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart b/test/_data/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart
new file mode 100644
index 0000000..21576d3
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/web_plugin/lib/main.dart
@@ -0,0 +1,3 @@
+import 'dart:html'; //OK
+import 'dart:js'; //OK
+import 'dart:js_util'; //OK
diff --git a/test/_data/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml b/test/_data/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml
new file mode 100644
index 0000000..1a55a4b
--- /dev/null
+++ b/test/_data/avoid_web_libraries_in_flutter/web_plugin/pubspec.yaml
@@ -0,0 +1,20 @@
+name: sample_project
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ cupertino_icons: ^0.1.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ plugin:
+ web:
+ pluginClass: SamplePlugin
+ fileName: main.dart
diff --git a/test/integration_test.dart b/test/integration_test.dart
index 80cf879..26b2e97 100644
--- a/test/integration_test.dart
+++ b/test/integration_test.dart
@@ -21,6 +21,70 @@
defineTests() {
group('integration', () {
+ group('avoid_web_libraries_in_flutter', () {
+ IOSink currentOut = outSink;
+ CollectingSink collectingOut = CollectingSink();
+ setUp(() {
+ exitCode = 0;
+ outSink = collectingOut;
+ });
+ tearDown(() {
+ collectingOut.buffer.clear();
+ outSink = currentOut;
+ exitCode = 0;
+ });
+
+ test('no pubspec', () async {
+ await cli.runLinter([
+ 'test/_data/avoid_web_libraries_in_flutter/no_pubspec',
+ '--rules=avoid_web_libraries_in_flutter',
+ ], LinterOptions());
+ expect(collectingOut.trim(),
+ contains('1 file analyzed, 0 issues found, in'));
+ expect(exitCode, 0);
+ });
+
+ test('non flutter app', () async {
+ await cli.runLinter([
+ 'test/_data/avoid_web_libraries_in_flutter/non_flutter_app',
+ '--rules=avoid_web_libraries_in_flutter',
+ ], LinterOptions());
+ expect(collectingOut.trim(),
+ contains('2 files analyzed, 0 issues found, in'));
+ expect(exitCode, 0);
+ });
+
+ test('non web app', () async {
+ await cli.runLinter([
+ 'test/_data/avoid_web_libraries_in_flutter/non_web_app',
+ '--rules=avoid_web_libraries_in_flutter',
+ ], LinterOptions());
+ expect(collectingOut.trim(),
+ contains('3 files analyzed, 3 issues found, in'));
+ expect(exitCode, 1);
+ });
+
+ test('web app', () async {
+ await cli.runLinter([
+ 'test/_data/avoid_web_libraries_in_flutter/web_app',
+ '--rules=avoid_web_libraries_in_flutter',
+ ], LinterOptions());
+ expect(collectingOut.trim(),
+ contains('2 files analyzed, 0 issues found, in'));
+ expect(exitCode, 0);
+ });
+
+ test('web plugin', () async {
+ await cli.runLinter([
+ 'test/_data/avoid_web_libraries_in_flutter/web_plugin',
+ '--rules=avoid_web_libraries_in_flutter',
+ ], LinterOptions());
+ expect(collectingOut.trim(),
+ contains('2 files analyzed, 0 issues found, in'));
+ expect(exitCode, 0);
+ });
+ });
+
group('p2', () {
IOSink currentOut = outSink;
CollectingSink collectingOut = CollectingSink();