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();