Specify analysis rules and conform to them

This commit is contained in:
Simon Binder 2019-02-04 21:01:50 +01:00
parent 9090ded541
commit 75b3b25117
22 changed files with 190 additions and 90 deletions

80
analysis_options.yaml Normal file
View File

@ -0,0 +1,80 @@
analyzer:
strong-mode:
implicit-casts: false
errors:
unused_element: error
unused_import: error
unused_local_variable: error
dead_code: error
override_on_non_overriding_method: error
linter:
rules:
- annotate_overrides
- avoid_empty_else
- avoid_function_literals_in_foreach_calls
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_relative_lib_imports
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_types_as_parameter_names
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_types
- cancel_subscriptions
- cascade_invocations
- comment_references
- constant_identifier_names
- control_flow_in_finally
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
- invariant_booleans
- iterable_contains_unrelated_type
- library_names
- library_prefixes
- list_remove_unrelated_type
- no_adjacent_strings_in_list
- no_duplicate_case_values
- non_constant_identifier_names
- null_closures
- omit_local_variable_types
- only_throw_errors
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_adjacent_string_concatenation
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_contains
- prefer_equal_for_default_values
- prefer_final_fields
- prefer_initializing_formals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_single_quotes
- prefer_typing_uninitialized_variables
- recursive_getters
- slash_for_doc_comments
- super_goes_last
- test_types_in_equals
- throw_in_finally
- type_init_formals
- unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_statements
- unnecessary_this
- unrelated_type_equality_checks
- use_rethrow_when_possible
- valid_regexps

1
sally/analysis_options.yaml Symbolic link
View File

@ -0,0 +1 @@
../analysis_options.yaml

View File

@ -9,7 +9,7 @@ class Where extends Component {
@override @override
void writeInto(GenerationContext context) { void writeInto(GenerationContext context) {
context.buffer.write("WHERE "); context.buffer.write('WHERE ');
predicate.writeInto(context); predicate.writeInto(context);
} }
} }

View File

@ -11,6 +11,6 @@ class SqlTypeSystem {
/// Returns the appropriate sql type for the dart type provided as the /// Returns the appropriate sql type for the dart type provided as the
/// generic parameter. /// generic parameter.
SqlType<T> forDartType<T>() { SqlType<T> forDartType<T>() {
return types.singleWhere((t) => t is SqlType<T>); return types.singleWhere((t) => t is SqlType<T>) as SqlType<T>;
} }
} }

View File

@ -8,17 +8,21 @@ Expression<BoolType> and(Expression<BoolType> a, Expression<BoolType> b) =>
Expression<BoolType> not(Expression<BoolType> a) => NotExpression(a); Expression<BoolType> not(Expression<BoolType> a) => NotExpression(a);
class AndExpression extends Expression<BoolType> with InfixOperator<BoolType> { class AndExpression extends Expression<BoolType> with InfixOperator<BoolType> {
@override
Expression<BoolType> left, right; Expression<BoolType> left, right;
final String operator = "AND"; @override
final String operator = 'AND';
AndExpression(this.left, this.right); AndExpression(this.left, this.right);
} }
class OrExpression extends Expression<BoolType> with InfixOperator<BoolType> { class OrExpression extends Expression<BoolType> with InfixOperator<BoolType> {
@override
Expression<BoolType> left, right; Expression<BoolType> left, right;
final String operator = "AND"; @override
final String operator = 'AND';
OrExpression(this.left, this.right); OrExpression(this.left, this.right);
} }

View File

@ -39,21 +39,24 @@ abstract class InfixOperator<T extends SqlType> implements Expression<T> {
} }
} }
enum ComparisonOperator { less, less_or_equal, equal, more_or_equal, more } enum ComparisonOperator { less, lessOrEqual, equal, moreOrEqual, more }
class Comparison extends InfixOperator<BoolType> { class Comparison extends InfixOperator<BoolType> {
static const Map<ComparisonOperator, String> operatorNames = { static const Map<ComparisonOperator, String> operatorNames = {
ComparisonOperator.less: '<', ComparisonOperator.less: '<',
ComparisonOperator.less_or_equal: '<=', ComparisonOperator.lessOrEqual: '<=',
ComparisonOperator.equal: '=', ComparisonOperator.equal: '=',
ComparisonOperator.more_or_equal: '>=', ComparisonOperator.moreOrEqual: '>=',
ComparisonOperator.more: '>' ComparisonOperator.more: '>'
}; };
@override
final Expression left; final Expression left;
@override
final Expression right; final Expression right;
final ComparisonOperator op; final ComparisonOperator op;
@override
final bool placeBrackets = false; final bool placeBrackets = false;
@override @override
@ -61,5 +64,5 @@ class Comparison extends InfixOperator<BoolType> {
Comparison(this.left, this.op, this.right); Comparison(this.left, this.op, this.right);
Comparison.equal(this.left, this.right) : this.op = ComparisonOperator.equal; Comparison.equal(this.left, this.right) : op = ComparisonOperator.equal;
} }

View File

@ -10,7 +10,7 @@ class Variable<T, S extends SqlType<T>> extends Expression<S> {
@override @override
void writeInto(GenerationContext context) { void writeInto(GenerationContext context) {
context.introduceVariable(value); context.introduceVariable(value);
context.buffer.write("?"); context.buffer.write('?');
} }
} }

View File

@ -27,7 +27,7 @@ class BoolType extends SqlType<bool> {
@override @override
String mapToSqlConstant(bool content) { String mapToSqlConstant(bool content) {
return content ? "1" : "0"; return content ? '1' : '0';
} }
@override @override
@ -40,7 +40,7 @@ class StringType extends SqlType<String> {
const StringType(); const StringType();
@override @override
String mapFromDatabaseResponse(response) => response; String mapFromDatabaseResponse(response) => response as String;
@override @override
String mapToSqlConstant(String content) { String mapToSqlConstant(String content) {
@ -56,7 +56,7 @@ class IntType extends SqlType<int> {
const IntType(); const IntType();
@override @override
int mapFromDatabaseResponse(response) => response; int mapFromDatabaseResponse(response) => response as int;
@override @override
String mapToSqlConstant(int content) => content.toString(); String mapToSqlConstant(int content) => content.toString();

View File

@ -5,6 +5,7 @@ import 'package:sally/src/runtime/structure/table_info.dart';
class SelectStatement<UserTable, DataType> extends Query<UserTable> { class SelectStatement<UserTable, DataType> extends Query<UserTable> {
@override @override
// ignore: overridden_fields
covariant TableInfo<UserTable, DataType> table; covariant TableInfo<UserTable, DataType> table;
SelectStatement(GeneratedDatabase database, this.table) SelectStatement(GeneratedDatabase database, this.table)

View File

@ -23,6 +23,7 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
class GeneratedTextColumn extends GeneratedColumn<String, StringType> class GeneratedTextColumn extends GeneratedColumn<String, StringType>
implements TextColumn { implements TextColumn {
@override
final String $name; final String $name;
GeneratedTextColumn(this.$name); GeneratedTextColumn(this.$name);
@ -34,6 +35,7 @@ class GeneratedTextColumn extends GeneratedColumn<String, StringType>
class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType> class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
implements BoolColumn { implements BoolColumn {
@override
final String $name; final String $name;
GeneratedBoolColumn(this.$name); GeneratedBoolColumn(this.$name);
@ -48,6 +50,7 @@ class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
class GeneratedIntColumn extends GeneratedColumn<int, IntType> class GeneratedIntColumn extends GeneratedColumn<int, IntType>
implements IntColumn { implements IntColumn {
@override
final String $name; final String $name;
GeneratedIntColumn(this.$name); GeneratedIntColumn(this.$name);

View File

@ -19,13 +19,16 @@ class GeneratedUsersTable extends Users with TableInfo<Users, UserDataObject> {
GeneratedUsersTable(this.db); GeneratedUsersTable(this.db);
IntColumn id = GeneratedIntColumn("id"); @override
TextColumn name = GeneratedTextColumn("name"); IntColumn id = GeneratedIntColumn('id');
BoolColumn isAwesome = GeneratedBoolColumn("is_awesome"); @override
TextColumn name = GeneratedTextColumn('name');
@override
BoolColumn isAwesome = GeneratedBoolColumn('is_awesome');
@override @override
List<Column<dynamic, SqlType>> get $columns => [id, name, isAwesome]; List<Column<dynamic, SqlType>> get $columns => [id, name, isAwesome];
@override @override
String get $tableName => "users"; String get $tableName => 'users';
@override @override
Users get asDslTable => this; Users get asDslTable => this;
@override @override
@ -36,7 +39,7 @@ class GeneratedUsersTable extends Users with TableInfo<Users, UserDataObject> {
class TestDatabase extends GeneratedDatabase { class TestDatabase extends GeneratedDatabase {
TestDatabase(QueryExecutor executor) TestDatabase(QueryExecutor executor)
: super(SqlTypeSystem.withDefaults(), executor); : super(const SqlTypeSystem.withDefaults(), executor);
GeneratedUsersTable get users => GeneratedUsersTable(this); GeneratedUsersTable get users => GeneratedUsersTable(this);
} }

View File

@ -17,40 +17,40 @@ void main() {
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([])); when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([]));
}); });
group("Generates SELECT statements", () { group('Generates SELECT statements', () {
test("generates simple statements", () { test('generates simple statements', () {
db.select(db.users).get(); db.select(db.users).get();
verify(executor.runSelect("SELECT * FROM users;", argThat(isEmpty))); verify(executor.runSelect('SELECT * FROM users;', argThat(isEmpty)));
}); });
test("generates limit statements", () { test('generates limit statements', () {
(db.select(db.users)..limit(10)).get(); (db.select(db.users)..limit(10)).get();
verify(executor.runSelect( verify(executor.runSelect(
"SELECT * FROM users LIMIT 10;", argThat(isEmpty))); 'SELECT * FROM users LIMIT 10;', argThat(isEmpty)));
}); });
test("generates like expressions", () { test('generates like expressions', () {
(db.select(db.users)..where((u) => u.name.like("Dash%"))).get(); (db.select(db.users)..where((u) => u.name.like('Dash%'))).get();
verify(executor verify(executor
.runSelect("SELECT * FROM users WHERE name LIKE ?;", ["Dash%"])); .runSelect('SELECT * FROM users WHERE name LIKE ?;', ['Dash%']));
}); });
test("generates complex predicates", () { test('generates complex predicates', () {
(db.select(db.users) (db.select(db.users)
..where((u) => ..where((u) =>
and(not(u.name.equalsVal("Dash")), (u.id.isBiggerThan(12))))) and(not(u.name.equalsVal('Dash')), (u.id.isBiggerThan(12)))))
.get(); .get();
verify(executor.runSelect( verify(executor.runSelect(
"SELECT * FROM users WHERE (NOT name = ?) AND (id > ?);", 'SELECT * FROM users WHERE (NOT name = ?) AND (id > ?);',
["Dash", 12])); ['Dash', 12]));
}); });
test("generates expressions from boolean fields", () { test('generates expressions from boolean fields', () {
(db.select(db.users)..where((u) => u.isAwesome)).get(); (db.select(db.users)..where((u) => u.isAwesome)).get();
verify(executor.runSelect( verify(executor.runSelect(
"SELECT * FROM users WHERE (is_awesome = 1);", argThat(isEmpty))); 'SELECT * FROM users WHERE (is_awesome = 1);', argThat(isEmpty)));
}); });
}); });

View File

@ -0,0 +1 @@
../analysis_options.yaml

View File

@ -3,4 +3,4 @@ import 'package:source_gen/source_gen.dart';
import 'package:sally_generator/src/sally_generator.dart'; import 'package:sally_generator/src/sally_generator.dart';
Builder sallyBuilder(BuilderOptions _) => Builder sallyBuilder(BuilderOptions _) =>
new SharedPartBuilder([SallyGenerator()], "sally"); SharedPartBuilder([SallyGenerator()], 'sally');

View File

@ -6,7 +6,7 @@ class SallyError {
final Element affectedElement; final Element affectedElement;
SallyError( SallyError(
{this.critical = false, this.message, this.affectedElement = null}); {this.critical = false, this.message, this.affectedElement});
} }
class ErrorStore { class ErrorStore {

View File

@ -54,6 +54,9 @@ class AutoIncrement extends ColumnFeature {
@override @override
bool operator ==(other) => other is AutoIncrement; bool operator ==(other) => other is AutoIncrement;
@override
int get hashCode => 1337420;
} }
abstract class LimitingTextLength extends ColumnFeature abstract class LimitingTextLength extends ColumnFeature

View File

@ -4,22 +4,22 @@ import 'package:sally_generator/src/model/specified_column.dart';
import 'package:sally_generator/src/parser/parser.dart'; import 'package:sally_generator/src/parser/parser.dart';
import 'package:sally_generator/src/sally_generator.dart'; import 'package:sally_generator/src/sally_generator.dart';
const String startInt = "integer"; const String startInt = 'integer';
const String startString = "text"; const String startString = 'text';
const String startBool = "boolean"; const String startBool = 'boolean';
// todo replace with set literal once dart supports it // todo replace with set literal once dart supports it
final Set<String> starters = [startInt, startString, startBool].toSet(); final Set<String> starters = [startInt, startString, startBool].toSet();
const String functionNamed = "named"; const String functionNamed = 'named';
const String functionPrimaryKey = "primaryKey"; const String functionPrimaryKey = 'primaryKey';
const String functionReferences = "references"; const String functionReferences = 'references';
const String functionAutoIncrement = "autoIncrement"; const String functionAutoIncrement = 'autoIncrement';
const String functionWithLength = "withLength"; const String functionWithLength = 'withLength';
const String errorMessage = "This getter does not create a valid column that " const String errorMessage = 'This getter does not create a valid column that '
"can be parsed by sally. Please refer to the readme from sally to see how " 'can be parsed by sally. Please refer to the readme from sally to see how '
"columns are formed. If you have any questions, feel free to raise an issue."; 'columns are formed. If you have any questions, feel free to raise an issue.';
class ColumnParser extends ParserBase { class ColumnParser extends ParserBase {
ColumnParser(SallyGenerator generator) : super(generator); ColumnParser(SallyGenerator generator) : super(generator);
@ -51,9 +51,9 @@ class ColumnParser extends ParserBase {
String foundStartMethod; String foundStartMethod;
String foundExplicitName; String foundExplicitName;
bool wasDeclaredAsPrimaryKey = false; var wasDeclaredAsPrimaryKey = false;
// todo parse reference // todo parse reference
List<ColumnFeature> foundFeatures = []; final foundFeatures = <ColumnFeature>[];
while (true) { while (true) {
final methodName = remainingExpr.methodName.name; final methodName = remainingExpr.methodName.name;
@ -71,7 +71,7 @@ class ColumnParser extends ParserBase {
affectedElement: getter.declaredElement, affectedElement: getter.declaredElement,
message: message:
"You're setting more than one name here, the first will " "You're setting more than one name here, the first will "
"be used")); 'be used'));
} }
foundExplicitName = foundExplicitName =
@ -80,8 +80,8 @@ class ColumnParser extends ParserBase {
critical: false, critical: false,
affectedElement: getter.declaredElement, affectedElement: getter.declaredElement,
message: message:
"This table name is cannot be resolved! Please only use " 'This table name is cannot be resolved! Please only use '
"a constant string as parameter for .named().")); 'a constant string as parameter for .named().'));
}); });
break; break;
case functionPrimaryKey: case functionPrimaryKey:
@ -91,8 +91,8 @@ class ColumnParser extends ParserBase {
break; // todo: parsing this is going to suck break; // todo: parsing this is going to suck
case functionWithLength: case functionWithLength:
final args = remainingExpr.argumentList; final args = remainingExpr.argumentList;
final minArg = findNamedArgument(args, "min"); final minArg = findNamedArgument(args, 'min');
final maxArg = findNamedArgument(args, "max"); final maxArg = findNamedArgument(args, 'max');
foundFeatures.add(LimitingTextLength.withLength( foundFeatures.add(LimitingTextLength.withLength(
min: readIntLiteral(minArg, () {}), min: readIntLiteral(minArg, () {}),

View File

@ -22,7 +22,7 @@ class ParserBase {
affectedElement: method.declaredElement, affectedElement: method.declaredElement,
critical: true, critical: true,
message: message:
"This method must have an expression body (use => instead of {return ...})")); 'This method must have an expression body (use => instead of {return ...})'));
return null; return null;
} }
@ -33,7 +33,7 @@ class ParserBase {
if (!(expression is StringLiteral)) { if (!(expression is StringLiteral)) {
onError(); onError();
} else { } else {
String value = (expression as StringLiteral).stringValue; final value = (expression as StringLiteral).stringValue;
if (value == null) if (value == null)
onError(); onError();
else else
@ -46,6 +46,7 @@ class ParserBase {
int readIntLiteral(Expression expression, void onError()) { int readIntLiteral(Expression expression, void onError()) {
if (!(expression is IntegerLiteral)) { if (!(expression is IntegerLiteral)) {
onError(); onError();
// ignore: avoid_returning_null
return null; return null;
} else { } else {
return (expression as IntegerLiteral).value; return (expression as IntegerLiteral).value;

View File

@ -12,19 +12,19 @@ class TableParser extends ParserBase {
TableParser(SallyGenerator generator) : super(generator); TableParser(SallyGenerator generator) : super(generator);
SpecifiedTable parse(ClassElement element) { SpecifiedTable parse(ClassElement element) {
String sqlName = _parseTableName(element); final sqlName = _parseTableName(element);
return SpecifiedTable( return SpecifiedTable(
fromClass: element, fromClass: element,
columns: _parseColumns(element), columns: _parseColumns(element),
sqlName: sqlName, sqlName: sqlName,
dartTypeName: dartTypeName:
"${element.name}_Data" // TODO better name for generated data classes '${element.name}_Data' // TODO better name for generated data classes
); );
} }
String _parseTableName(ClassElement element) { String _parseTableName(ClassElement element) {
final tableNameGetter = element.getGetter("tableName"); final tableNameGetter = element.getGetter('tableName');
if (tableNameGetter == null) { if (tableNameGetter == null) {
// class does not override tableName. So just use the dart class name // class does not override tableName. So just use the dart class name
// instead. Will use placed_orders for a class called PlacedOrders // instead. Will use placed_orders for a class called PlacedOrders
@ -38,11 +38,11 @@ class TableParser extends ParserBase {
final returnExpr = returnExpressionOfMethod( final returnExpr = returnExpressionOfMethod(
tableNameDeclaration.node as MethodDeclaration); tableNameDeclaration.node as MethodDeclaration);
String tableName = readStringLiteral(returnExpr, () { final tableName = readStringLiteral(returnExpr, () {
generator.errors.add(SallyError( generator.errors.add(SallyError(
critical: true, critical: true,
message: message:
"This getter must return a string literal, and do nothing more", 'This getter must return a string literal, and do nothing more',
affectedElement: tableNameGetter)); affectedElement: tableNameGetter));
}); });

View File

@ -8,8 +8,8 @@ import 'package:sally_generator/src/parser/table_parser.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
class SallyGenerator extends Generator { class SallyGenerator extends Generator {
Map<String, ParsedLibraryResult> _astForLibs = Map(); final Map<String, ParsedLibraryResult> _astForLibs = {};
ErrorStore errors = ErrorStore(); final ErrorStore errors = ErrorStore();
TableParser tableParser; TableParser tableParser;
ColumnParser columnParser; ColumnParser columnParser;
@ -25,12 +25,12 @@ class SallyGenerator extends Generator {
@override @override
String generate(LibraryReader library, BuildStep buildStep) { String generate(LibraryReader library, BuildStep buildStep) {
final testUsers = library.findType("Users"); final testUsers = library.findType('Users');
if (testUsers == null) return ""; if (testUsers == null) return '';
TableParser(this).parse(testUsers); TableParser(this).parse(testUsers);
return ""; return '';
} }
} }

View File

@ -1,9 +1,9 @@
import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/dart/element/type.dart';
bool isFromSally(DartType type) { bool isFromSally(DartType type) {
return type.element.library.location.components.first.contains("sally"); return type.element.library.location.components.first.contains('sally');
} }
bool isColumn(DartType type) { bool isColumn(DartType type) {
return isFromSally(type) && type.name.contains("Column"); return isFromSally(type) && type.name.contains('Column');
} }

View File

@ -36,7 +36,7 @@ void main() async {
@override @override
String get tableName => constructTableName();" String get tableName => constructTableName();"
} }
''', (r) => r.findLibraryByName("test_parser")); ''', (r) => r.findLibraryByName('test_parser'));
}); });
setUp(() { setUp(() {
@ -44,57 +44,57 @@ void main() async {
generator.columnParser = ColumnParser(generator); generator.columnParser = ColumnParser(generator);
}); });
group("SQL table name", () { group('SQL table name', () {
test("should parse correctly when valid", () { test('should parse correctly when valid', () {
expect( expect(
TableParser(generator) TableParser(generator)
.parse(testLib.getType("TableWithCustomName")) .parse(testLib.getType('TableWithCustomName'))
.sqlName, .sqlName,
equals("my-fancy-table")); equals('my-fancy-table'));
}); });
test("should use class name if table name is not specified", () { test('should use class name if table name is not specified', () {
expect(TableParser(generator).parse(testLib.getType("Users")).sqlName, expect(TableParser(generator).parse(testLib.getType('Users')).sqlName,
equals("users")); equals('users'));
}); });
test("should not parse for complex methods", () async { test('should not parse for complex methods', () async {
TableParser(generator).parse(testLib.getType("WrongName")); TableParser(generator).parse(testLib.getType('WrongName'));
expect(generator.errors.errors, isNotEmpty); expect(generator.errors.errors, isNotEmpty);
}); });
}); });
group("Columns", () { group('Columns', () {
test("should use field name if no name has been set explicitely", () { test('should use field name if no name has been set explicitely', () {
final table = TableParser(generator).parse(testLib.getType("Users")); final table = TableParser(generator).parse(testLib.getType('Users'));
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == "id"); table.columns.singleWhere((col) => col.name.name == 'id');
expect(idColumn.name, equals(ColumnName.implicitly("id"))); expect(idColumn.name, equals(ColumnName.implicitly('id')));
}); });
test("should use explicit name, if it exists", () { test('should use explicit name, if it exists', () {
final table = TableParser(generator).parse(testLib.getType("Users")); final table = TableParser(generator).parse(testLib.getType('Users'));
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == "user_name"); table.columns.singleWhere((col) => col.name.name == 'user_name');
expect(idColumn.name, equals(ColumnName.explicitly("user_name"))); expect(idColumn.name, equals(ColumnName.explicitly('user_name')));
}); });
test("should parse min and max length for text columns", () { test('should parse min and max length for text columns', () {
final table = TableParser(generator).parse(testLib.getType("Users")); final table = TableParser(generator).parse(testLib.getType('Users'));
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == "user_name"); table.columns.singleWhere((col) => col.name.name == 'user_name');
expect(idColumn.features, expect(idColumn.features,
contains(LimitingTextLength.withLength(min: 6, max: 32))); contains(LimitingTextLength.withLength(min: 6, max: 32)));
}); });
test("should only parse max length when relevant", () { test('should only parse max length when relevant', () {
final table = TableParser(generator).parse(testLib.getType("Users")); final table = TableParser(generator).parse(testLib.getType('Users'));
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == "onlyMax"); table.columns.singleWhere((col) => col.name.name == 'onlyMax');
expect( expect(
idColumn.features, contains(LimitingTextLength.withLength(max: 100))); idColumn.features, contains(LimitingTextLength.withLength(max: 100)));