[linting_tool] Implement saving rules to DB (#860)

pull/866/head
Abdullah Deshmukh 3 years ago committed by GitHub
parent 1818925286
commit bbb8e342f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,6 +3,7 @@ include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/model/rule.g.dart
- test/widget_test.mocks.dart
strong-mode:
implicit-casts: false
implicit-dynamic: false

@ -3,11 +3,13 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:linting_tool/model/profiles_store.dart';
import 'package:linting_tool/model/rules_store.dart';
import 'package:linting_tool/theme/app_theme.dart';
import 'package:linting_tool/widgets/adaptive_nav.dart';
import 'package:linting_tool/routes.dart' as routes;
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
class LintingTool extends StatefulWidget {
const LintingTool({Key? key}) : super(key: key);
@ -21,8 +23,15 @@ class LintingTool extends StatefulWidget {
class _LintingToolState extends State<LintingTool> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<RuleStore>(
create: (context) => RuleStore(),
return MultiProvider(
providers: [
ChangeNotifierProvider<RuleStore>(
create: (context) => RuleStore(http.Client()),
),
ChangeNotifierProvider<ProfilesStore>(
create: (context) => ProfilesStore(),
),
],
child: MaterialApp(
title: 'Flutter Linting Tool',
theme: AppTheme.buildReplyLightTheme(context),

@ -3,8 +3,15 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:linting_tool/app.dart';
import 'package:linting_tool/model/profile.dart';
import 'package:linting_tool/model/rule.dart';
void main() {
Future<void> main() async {
await Hive.initFlutter();
Hive.registerAdapter(RuleAdapter());
Hive.registerAdapter(RulesProfileAdapter());
await Hive.openLazyBox<RulesProfile>('rules_profile');
runApp(const LintingTool());
}

@ -0,0 +1,26 @@
// Copyright 2021 The Flutter team. 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:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:linting_tool/model/rule.dart';
part 'profile.g.dart';
@HiveType(typeId: 1)
class RulesProfile extends Equatable {
@HiveField(0)
final String name;
@HiveField(1)
final List<Rule> rules;
const RulesProfile({
required this.name,
required this.rules,
});
@override
List<Object?> get props => [name];
}

@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'profile.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RulesProfileAdapter extends TypeAdapter<RulesProfile> {
@override
final int typeId = 1;
@override
RulesProfile read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RulesProfile(
name: fields[0] as String,
rules: (fields[1] as List).cast<Rule>(),
);
}
@override
void write(BinaryWriter writer, RulesProfile obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.rules);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RulesProfileAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

@ -0,0 +1,69 @@
// Copyright 2021 The Flutter team. 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:developer';
import 'package:flutter/material.dart';
import 'package:linting_tool/model/profile.dart';
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/repository/hive_service.dart';
const _boxName = 'rules_profile';
class ProfilesStore extends ChangeNotifier {
ProfilesStore() {
fetchSavedProfiles();
}
bool _isLoading = true;
bool get isLoading => _isLoading;
List<RulesProfile> _savedProfiles = [];
List<RulesProfile> get savedProfiles => _savedProfiles;
String? _error;
String? get error => _error;
Future<void> fetchSavedProfiles() async {
if (!_isLoading) _isLoading = true;
notifyListeners();
try {
var profiles = await HiveService.getBoxes<RulesProfile>(_boxName);
_savedProfiles = profiles;
} on Exception catch (e) {
log(e.toString());
}
_isLoading = false;
notifyListeners();
}
Future<void> addToNewProfile(RulesProfile profile) async {
await HiveService.addBox<RulesProfile>(profile, _boxName);
await Future.delayed(const Duration(milliseconds: 100), () async {
await fetchSavedProfiles();
});
}
Future<void> addToExistingProfile(RulesProfile profile, Rule rule) async {
RulesProfile newProfile =
RulesProfile(name: profile.name, rules: profile.rules..add(rule));
await HiveService.updateBox<RulesProfile>(profile, newProfile, _boxName);
await Future.delayed(const Duration(milliseconds: 100), () async {
await fetchSavedProfiles();
});
}
Future<void> deleteProfile(RulesProfile profile) async {
await HiveService.deleteBox<RulesProfile>(profile, _boxName);
await Future.delayed(const Duration(milliseconds: 100), () async {
await fetchSavedProfiles();
});
}
}

@ -3,18 +3,27 @@
// found in the LICENSE file.
import 'package:equatable/equatable.dart';
import 'package:hive/hive.dart';
import 'package:json_annotation/json_annotation.dart';
part 'rule.g.dart';
@JsonSerializable()
@HiveType(typeId: 0)
class Rule extends Equatable {
@HiveField(0)
final String name;
@HiveField(1)
final String description;
@HiveField(2)
final String group;
@HiveField(3)
final String maturity;
@HiveField(4)
final List<String> incompatible;
@HiveField(5)
final List<String> sets;
@HiveField(6)
final String details;
const Rule({

@ -2,6 +2,62 @@
part of 'rule.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RuleAdapter extends TypeAdapter<Rule> {
@override
final int typeId = 0;
@override
Rule read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Rule(
name: fields[0] as String,
description: fields[1] as String,
group: fields[2] as String,
maturity: fields[3] as String,
incompatible: (fields[4] as List).cast<String>(),
sets: (fields[5] as List).cast<String>(),
details: fields[6] as String,
);
}
@override
void write(BinaryWriter writer, Rule obj) {
writer
..writeByte(7)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.description)
..writeByte(2)
..write(obj.group)
..writeByte(3)
..write(obj.maturity)
..writeByte(4)
..write(obj.incompatible)
..writeByte(5)
..write(obj.sets)
..writeByte(6)
..write(obj.details);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RuleAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

@ -8,9 +8,13 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/repository/repository.dart';
import 'package:http/http.dart' as http;
class RuleStore extends ChangeNotifier {
RuleStore() {
late final Repository repository;
RuleStore(http.Client httpClient) {
repository = Repository(httpClient);
fetchRules();
}
bool _isLoading = true;
@ -29,7 +33,7 @@ class RuleStore extends ChangeNotifier {
if (!_isLoading) _isLoading = true;
notifyListeners();
try {
var rules = await Repository().getRulesList();
var rules = await repository.getRulesList();
_rules = rules;
} on SocketException catch (e) {
log(e.toString());

@ -0,0 +1,74 @@
// Copyright 2021 The Flutter team. 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:flutter/material.dart';
import 'package:linting_tool/layout/adaptive.dart';
import 'package:linting_tool/model/profile.dart';
import 'package:linting_tool/widgets/saved_rule_tile.dart';
class RulesPage extends StatelessWidget {
final RulesProfile profile;
const RulesPage({
required this.profile,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDesktop = isDisplayLarge(context);
final isTablet = isDisplayMedium(context);
var textTheme = Theme.of(context).textTheme;
final startPadding = isTablet
? 60.0
: isDesktop
? 120.0
: 4.0;
final endPadding = isTablet
? 60.0
: isDesktop
? 120.0
: 4.0;
return Scaffold(
appBar: AppBar(
title: Text(
profile.name,
style: textTheme.subtitle2!.copyWith(
color: textTheme.bodyText1!.color,
),
),
leading: Padding(
padding: const EdgeInsets.only(left: 80.0),
child: TextButton.icon(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back_ios_new),
label: const Text('Back'),
),
),
leadingWidth: 160.0,
toolbarHeight: 38.0,
backgroundColor: Colors.white,
brightness: Brightness.light,
),
body: ListView.separated(
padding: EdgeInsetsDirectional.only(
start: startPadding,
end: endPadding,
top: isDesktop ? 28 : 0,
bottom: isDesktop ? kToolbarHeight : 0,
),
itemCount: profile.rules.length,
cacheExtent: 5,
itemBuilder: (context, index) {
return SavedRuleTile(
rule: profile.rules[index],
);
},
separatorBuilder: (context, index) => const SizedBox(height: 4),
),
);
}
}

@ -3,13 +3,124 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:linting_tool/layout/adaptive.dart';
import 'package:linting_tool/model/profiles_store.dart';
import 'package:linting_tool/pages/rules_page.dart';
import 'package:linting_tool/theme/colors.dart';
import 'package:provider/provider.dart';
class SavedLintsPage extends StatelessWidget {
const SavedLintsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO(abd99): Implement SavedLintsPage, showing a list of saved lint rules profiles.
return const Text('Saved Profiles');
return Consumer<ProfilesStore>(
builder: (context, profilesStore, child) {
if (profilesStore.isLoading) {
return const CircularProgressIndicator.adaptive();
}
if (!profilesStore.isLoading) {
if (profilesStore.savedProfiles.isNotEmpty) {
final isDesktop = isDisplayLarge(context);
final isTablet = isDisplayMedium(context);
final startPadding = isTablet
? 60.0
: isDesktop
? 120.0
: 4.0;
final endPadding = isTablet
? 60.0
: isDesktop
? 120.0
: 4.0;
return ListView.separated(
padding: EdgeInsetsDirectional.only(
start: startPadding,
end: endPadding,
top: isDesktop ? 28 : 0,
bottom: isDesktop ? kToolbarHeight : 0,
),
itemCount: profilesStore.savedProfiles.length,
cacheExtent: 5,
itemBuilder: (context, index) {
var profile = profilesStore.savedProfiles[index];
return ListTile(
title: Text(
profile.name,
),
tileColor: AppColors.white50,
onTap: () {
Navigator.push<void>(
context,
MaterialPageRoute(
builder: (context) => RulesPage(profile: profile),
),
);
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
// TODO(abd99): Implement edit functionality.
},
),
const SizedBox(
width: 8.0,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
switch (value) {
case 'Export file':
// TODO(abd99): Implement exporting files.
break;
case 'Delete':
profilesStore.deleteProfile(profile);
break;
default:
}
},
itemBuilder: (context) {
return [
const PopupMenuItem(
child: Text('Export file'),
value: 'Export file',
),
const PopupMenuItem(
child: Text('Delete'),
value: 'Delete',
),
];
},
),
],
),
);
},
separatorBuilder: (context, index) => const SizedBox(height: 4),
);
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(profilesStore.error ?? 'No saved profiles found.'),
const SizedBox(
height: 16.0,
),
IconButton(
onPressed: () => profilesStore.fetchSavedProfiles(),
icon: const Icon(Icons.refresh),
),
],
);
},
);
}
}

@ -8,9 +8,12 @@ import 'package:linting_tool/model/rule.dart';
class APIProvider {
final _baseURL = 'https://dart-lang.github.io/linter';
final http.Client httpClient;
APIProvider(this.httpClient);
Future<List<Rule>> getRulesList() async {
http.Response response =
await http.get(Uri.parse('$_baseURL//lints/machine/rules.json'));
await httpClient.get(Uri.parse('$_baseURL//lints/machine/rules.json'));
if (response.statusCode == 200) {
List<Rule> rulesList = [];

@ -0,0 +1,77 @@
// Copyright 2021 The Flutter team. 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:hive/hive.dart';
class HiveService {
static Future<bool> addBox<T>(T item, String boxName) async {
final openBox = await Hive.openLazyBox<T>(
boxName,
);
List<T> existingProducts = await getBoxes(boxName);
if (!existingProducts.contains(item)) {
openBox.add(item);
return true;
}
return false;
}
static Future addBoxes<T>(List<T> items, String boxName) async {
final openBox = await Hive.openLazyBox<T>(
boxName,
);
List<T> existingProducts = await getBoxes(boxName);
for (var item in items) {
if (!existingProducts.contains(item)) {
openBox.add(item);
}
}
}
static Future deleteBox<T>(T item, String boxName) async {
final openBox = await Hive.openLazyBox<T>(
boxName,
);
List<T> boxes = await getBoxes(boxName);
for (var box in boxes) {
if (box == item) {
openBox.deleteAt(boxes.indexOf(item));
}
}
}
static Future updateBox<T>(T item, T newItem, String boxName) async {
final openBox = await Hive.openLazyBox<T>(
boxName,
);
List<T> boxes = await getBoxes(boxName);
for (var box in boxes) {
if (box == item) {
openBox.putAt(boxes.indexOf(item), newItem);
}
}
}
static Future<List<T>> getBoxes<T>(String boxName, [String? query]) async {
List<T> boxList = [];
final openBox = await Hive.openLazyBox<T>(boxName);
int length = openBox.length;
for (int i = 0; i < length; i++) {
boxList.add(await openBox.getAt(i) as T);
}
return boxList;
}
}

@ -4,9 +4,14 @@
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/repository/api_provider.dart';
import 'package:http/http.dart' as http;
class Repository {
final _apiProvider = APIProvider();
late final APIProvider _apiProvider;
Repository(http.Client httpClient) {
_apiProvider = APIProvider(httpClient);
}
Future<List<Rule>> getRulesList() => _apiProvider.getRulesList();
}

@ -91,7 +91,19 @@ class _NavViewState extends State<_NavView> {
@override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(
title: Text(
'Flutter Linting Tool',
style: textTheme.subtitle2!.copyWith(
color: textTheme.bodyText1!.color,
),
),
toolbarHeight: 38.0,
backgroundColor: Colors.white,
brightness: Brightness.light,
),
body: Row(
children: [
LayoutBuilder(

@ -4,9 +4,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:linting_tool/model/profile.dart';
import 'package:linting_tool/model/profiles_store.dart';
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/theme/app_theme.dart';
import 'package:linting_tool/theme/colors.dart';
import 'package:provider/provider.dart';
class LintExpansionTile extends StatefulWidget {
final Rule rule;
@ -127,8 +130,30 @@ class _LintExpansionTileState extends State<LintExpansionTile> {
alignment: Alignment.centerRight,
child: ElevatedButton(
child: const Text('Add to profile'),
onPressed: () {
// TODO(abd99): Iplement adding to a profile.
onPressed: () async {
ProfileType? destinationProfileType =
await showDialog<ProfileType>(
context: context,
builder: (context) {
return const _ProfileTypeDialog();
},
);
if (destinationProfileType == ProfileType.newProfile) {
showDialog<String>(
context: context,
builder: (context) {
return _NewProfileDialog(rule: rule);
},
);
} else if (destinationProfileType ==
ProfileType.existingProfile) {
showDialog<String>(
context: context,
builder: (context) {
return _ExistingProfileDialog(rule: rule);
},
);
}
},
),
),
@ -139,3 +164,145 @@ class _LintExpansionTileState extends State<LintExpansionTile> {
);
}
}
enum ProfileType {
newProfile,
existingProfile,
}
class _ProfileTypeDialog extends StatelessWidget {
const _ProfileTypeDialog({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
actionsPadding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 16.0,
),
title: const Text('Select Profile Type'),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context, ProfileType.existingProfile);
},
child: const Text('Existing Profile'),
),
TextButton(
onPressed: () {
Navigator.pop(context, ProfileType.newProfile);
},
child: const Text('Create new profile'),
),
],
);
}
}
class _NewProfileDialog extends StatelessWidget {
final Rule rule;
const _NewProfileDialog({
required this.rule,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String name = '';
final _formKey = GlobalKey<FormState>();
return AlertDialog(
title: const Text('Create new lint profile'),
content: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Profile Name'),
TextFormField(
onChanged: (value) {
name = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Name cannot be empty.';
}
return null;
},
),
],
),
),
actionsPadding: const EdgeInsets.all(16.0),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
var newProfile = RulesProfile(
name: name,
rules: [rule],
);
await Provider.of<ProfilesStore>(context, listen: false)
.addToNewProfile(newProfile);
Navigator.pop(context);
}
},
child: const Text('Save'),
),
],
);
}
}
class _ExistingProfileDialog extends StatelessWidget {
const _ExistingProfileDialog({
Key? key,
required this.rule,
}) : super(key: key);
final Rule rule;
@override
Widget build(BuildContext context) {
var profilesStore = Provider.of<ProfilesStore>(context);
var savedProfiles = profilesStore.savedProfiles;
return AlertDialog(
title: const Text('Select a lint profile'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
savedProfiles.length,
(index) => ListTile(
title: Text(savedProfiles[index].name),
onTap: () async {
await profilesStore.addToExistingProfile(
savedProfiles[index], rule);
Navigator.pop(context);
},
),
),
),
actionsPadding: const EdgeInsets.all(16.0),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
],
);
}
}

@ -0,0 +1,132 @@
// Copyright 2021 The Flutter team. 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:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/theme/app_theme.dart';
import 'package:linting_tool/theme/colors.dart';
class SavedRuleTile extends StatefulWidget {
final Rule rule;
const SavedRuleTile({
required this.rule,
Key? key,
}) : super(key: key);
@override
_SavedRuleTileState createState() => _SavedRuleTileState();
}
class _SavedRuleTileState extends State<SavedRuleTile> {
var isExpanded = false;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var textTheme = theme.textTheme;
final rule = widget.rule;
final incompatibleString =
rule.incompatible.isNotEmpty ? rule.incompatible.join(', ') : 'none';
final setsString = rule.sets.isNotEmpty ? rule.sets.join(', ') : 'none';
// TODO(abd99): Add option to remove rule from profile.
// TODO(abd99): Add right click functionality.
return ExpansionTile(
collapsedBackgroundColor: AppColors.white50,
title: Text(
rule.name,
style: textTheme.subtitle1!.copyWith(
fontWeight: FontWeight.w700,
),
),
subtitle: Text(
rule.description,
style: textTheme.caption!,
),
initiallyExpanded: isExpanded,
onExpansionChanged: (value) {
setState(() {
isExpanded = value;
});
},
expandedAlignment: Alignment.centerLeft,
childrenPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
backgroundColor: AppColors.white50,
maintainState: true,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Group:',
style: textTheme.subtitle2,
),
TextSpan(
text: ' ${rule.group}',
),
],
),
textAlign: TextAlign.left,
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Maturity:',
style: textTheme.subtitle2,
),
TextSpan(
text: ' ${rule.maturity}',
),
],
),
textAlign: TextAlign.left,
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Incompatible:',
style: textTheme.subtitle2,
),
TextSpan(
text: ' $incompatibleString',
),
],
),
textAlign: TextAlign.left,
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Sets:',
style: textTheme.subtitle2,
),
TextSpan(
text: ' $setsString',
),
],
),
textAlign: TextAlign.left,
),
const SizedBox(
height: 16.0,
),
MarkdownBody(
data: rule.details,
selectable: true,
styleSheet: AppTheme.buildMarkDownTheme(theme),
),
const SizedBox(
height: 16.0,
),
],
);
}
}

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -13,7 +13,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="linting_tool" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
@ -326,14 +326,15 @@
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="linting_tool" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<point key="canvasLocation" x="7" y="-655"/>
</window>
</objects>
</document>

@ -263,6 +263,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
hive:
dependency: "direct main"
description:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
hive_flutter:
dependency: "direct main"
description:
name: hive_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
hive_generator:
dependency: "direct dev"
description:
name: hive_generator
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
http:
dependency: "direct main"
description:
@ -354,6 +375,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
mockito:
dependency: "direct main"
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.13"
nested:
dependency: transitive
description:
@ -492,6 +520,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
source_helper:
dependency: transitive
description:
name: source_helper
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
source_span:
dependency: transitive
description:

@ -14,8 +14,11 @@ dependencies:
equatable: ^2.0.3
flutter_markdown: ^0.6.2
google_fonts: ^2.1.0
hive: ^2.0.4
hive_flutter: ^1.1.0
http: ^0.13.3
json_annotation: ^4.0.1
mockito: ^5.0.13
provider: ^5.0.0
dev_dependencies:
@ -23,6 +26,7 @@ dev_dependencies:
sdk: flutter
build_runner: ^2.0.6
flutter_lints: ^1.0.3
hive_generator: ^1.1.0
json_serializable: ^4.1.4
flutter:

@ -1,20 +1,88 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
// Copyright 2021 The Flutter team. 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:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:linting_tool/app.dart';
import 'package:linting_tool/model/profile.dart';
import 'package:linting_tool/model/profiles_store.dart';
import 'package:linting_tool/model/rule.dart';
import 'package:linting_tool/model/rules_store.dart';
import 'package:linting_tool/pages/default_lints_page.dart';
import 'package:linting_tool/pages/home_page.dart';
import 'package:linting_tool/pages/saved_lints_page.dart';
import 'package:linting_tool/theme/app_theme.dart';
import 'package:linting_tool/widgets/adaptive_nav.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import 'widget_test.mocks.dart';
late MockClient _mockClient;
class _TestApp extends StatelessWidget {
const _TestApp({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<RuleStore>(
create: (context) => RuleStore(_mockClient),
),
ChangeNotifierProvider<ProfilesStore>(
create: (context) => ProfilesStore(),
),
],
child: MaterialApp(
title: 'Flutter Linting Tool',
initialRoute: LintingTool.homeRoute,
theme: AppTheme.buildReplyLightTheme(context),
onGenerateRoute: (settings) {
switch (settings.name) {
case LintingTool.homeRoute:
return MaterialPageRoute<void>(
builder: (context) => const AdaptiveNav(),
settings: settings,
);
}
return null;
},
),
);
}
}
@GenerateMocks([http.Client])
void main() {
setUp(() async {
final tempDir = await Directory.systemTemp.createTemp();
Hive.init(tempDir.path);
Hive.registerAdapter(RuleAdapter());
Hive.registerAdapter(RulesProfileAdapter());
await Hive.openLazyBox<RulesProfile>('rules_profile');
_mockClient = MockClient();
});
testWidgets('NavigationRail smoke test', (tester) async {
await tester.pumpWidget(const LintingTool());
var responseBody =
'''[{"name": "always_use_package_imports","description": "Avoid relative imports for files in `lib/`.","group": "errors","maturity": "stable","incompatible": [],"sets": [],"details": "*DO* avoid relative imports for files in `lib/`.\n\nWhen mixing relative and absolute imports it's possible to create confusion\nwhere the same member gets imported in two different ways. One way to avoid\nthat is to ensure you consistently use absolute imports for files withing the\n`lib/` directory.\n\nThis is the opposite of 'prefer_relative_imports'.\nMight be used with 'avoid_relative_lib_imports' to avoid relative imports of\nfiles within `lib/` directory outside of it. (for example `test/`)\n\n**GOOD:**\n\n```dart\nimport 'package:foo/bar.dart';\n\nimport 'package:foo/baz.dart';\n\nimport 'package:foo/src/baz.dart';\n...\n```\n\n**BAD:**\n\n```dart\nimport 'baz.dart';\n\nimport 'src/bag.dart'\n\nimport '../lib/baz.dart';\n\n...\n```\n\n"}]''';
when(_mockClient.get(Uri.parse(
'https://dart-lang.github.io/linter//lints/machine/rules.json')))
.thenAnswer(
(_) async => http.Response(responseBody, 400),
);
await tester.pumpWidget(const _TestApp());
expect(find.byType(HomePage), findsOneWidget);
expect(find.byType(SavedLintsPage), findsNothing);
await tester.tap(find.text('Saved Profiles'));
await tester.pumpAndSettle();

@ -0,0 +1,107 @@
// Mocks generated by Mockito 5.0.13 from annotations
// in linting_tool/test/widget_test.dart.
// Do not manually edit this file.
import 'dart:async' as _i5;
import 'dart:convert' as _i6;
import 'dart:typed_data' as _i7;
import 'package:http/src/base_request.dart' as _i8;
import 'package:http/src/client.dart' as _i4;
import 'package:http/src/response.dart' as _i2;
import 'package:http/src/streamed_response.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
class _FakeResponse extends _i1.Fake implements _i2.Response {}
class _FakeStreamedResponse extends _i1.Fake implements _i3.StreamedResponse {}
/// A class which mocks [Client].
///
/// See the documentation for Mockito's code generation for more information.
class MockClient extends _i1.Mock implements _i4.Client {
MockClient() {
_i1.throwOnMissingStub(this);
}
@override
_i5.Future<_i2.Response> head(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#head, [url], {#headers: headers}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> get(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#get, [url], {#headers: headers}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> post(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#post, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> put(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#put, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> patch(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#patch, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> delete(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#delete, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse()))
as _i5.Future<_i2.Response>);
@override
_i5.Future<String> read(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#read, [url], {#headers: headers}),
returnValue: Future<String>.value('')) as _i5.Future<String>);
@override
_i5.Future<_i7.Uint8List> readBytes(Uri? url,
{Map<String, String>? headers}) =>
(super.noSuchMethod(
Invocation.method(#readBytes, [url], {#headers: headers}),
returnValue: Future<_i7.Uint8List>.value(_i7.Uint8List(0)))
as _i5.Future<_i7.Uint8List>);
@override
_i5.Future<_i3.StreamedResponse> send(_i8.BaseRequest? request) =>
(super.noSuchMethod(Invocation.method(#send, [request]),
returnValue:
Future<_i3.StreamedResponse>.value(_FakeStreamedResponse()))
as _i5.Future<_i3.StreamedResponse>);
@override
void close() => super.noSuchMethod(Invocation.method(#close, []),
returnValueForMissingStub: null);
@override
String toString() => super.toString();
}
Loading…
Cancel
Save