-
Notifications
You must be signed in to change notification settings - Fork 58
/
Copy pathmembers_from_ast.dart
515 lines (443 loc) · 19.3 KB
/
members_from_ast.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
// Copyright 2020 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import '../util.dart';
import 'ast_util.dart';
import 'members.dart';
import 'util.dart';
import 'version.dart';
/// A pattern that can detect a props class or mixin (assuming it follows naming convention).
final propsOrMixinNamePattern = RegExp(r'Props(?:Mixin)?$');
/// A pattern that can detect a props mixin (assuming it follows naming convention).
final propsMixinNamePattern = propsOrMixinNamePattern;
/// A pattern that can detect a state class or mixin (assuming it follows naming convention).
final stateMixinNamePattern = RegExp(r'State(?:Mixin)?$');
/// A pattern that can detect a props class (assuming it follows naming convention).
final propsNamePattern = RegExp(r'Props$');
/// A pattern that can detect a state class (assuming it follows naming convention).
final stateNamePattern = RegExp(r'State$');
/// Returns an unmodifiable collection of all potential boilerplate members
/// detected within [unit], with appropriate [BoilerplateMember.versionConfidences] scores.
BoilerplateMembers detectBoilerplateMembers(CompilationUnit unit) {
final factories = <BoilerplateFactory>[];
final props = <BoilerplateProps>[];
final propsMixins = <BoilerplatePropsMixin>[];
final components = <BoilerplateComponent>[];
final states = <BoilerplateState>[];
final stateMixins = <BoilerplateStateMixin>[];
_BoilerplateMemberDetector(
onFactory: factories.add,
onProps: props.add,
onPropsMixin: propsMixins.add,
onComponent: components.add,
onState: states.add,
onStateMixin: stateMixins.add,
).detect(unit);
return BoilerplateMembers(
factories: UnmodifiableListView(factories),
props: UnmodifiableListView(props),
propsMixins: UnmodifiableListView(propsMixins),
components: UnmodifiableListView(components),
states: UnmodifiableListView(states),
stateMixins: UnmodifiableListView(stateMixins),
);
}
/// Helper class that contains logic for detecting boilerplate entities for a given compilation unit
/// and determining their version confidences.
///
/// See: [VersionConfidences], [BoilerplateMember].
class _BoilerplateMemberDetector {
Map<String, NamedCompilationUnitMember>? _classishDeclarationsByName;
// Callbacks that will be triggered when the detector finds the correlating entity.
final void Function(BoilerplateFactory) onFactory;
final void Function(BoilerplateProps) onProps;
final void Function(BoilerplateState) onState;
final void Function(BoilerplatePropsMixin) onPropsMixin;
final void Function(BoilerplateStateMixin) onStateMixin;
final void Function(BoilerplateComponent) onComponent;
_BoilerplateMemberDetector({
required this.onFactory,
required this.onProps,
required this.onState,
required this.onPropsMixin,
required this.onStateMixin,
required this.onComponent,
});
/// Process [unit] looking for boilerplate members, calling the appropriate 'on'
/// methods (e.g. [onFactory]) upon discovery.
///
/// Uses [_BoilerplateMemberDetectorVisitor] to visit the relevant entity types,
/// looking for boilerplate members.
void detect(CompilationUnit unit) {
_classishDeclarationsByName = {};
final visitor = _BoilerplateMemberDetectorVisitor(
onClassishDeclaration: (node) => _classishDeclarationsByName![node.name.name] = node,
onTopLevelVariableDeclaration: _processTopLevelVariableDeclaration,
);
unit.accept(visitor);
_classishDeclarationsByName!.values.forEach(_processClassishDeclaration);
_classishDeclarationsByName = null;
}
void _processTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
_detectFactory(node);
}
void _processClassishDeclaration(NamedCompilationUnitMember node) {
// If this is a companion class, ignore it.
final sourceClass = _getSourceClassForPotentialCompanion(node);
if (sourceClass != null) return;
if (_isMixinStub(node)) return;
final classish = node.asClassish();
final companion = _getCompanionClass(node)?.asClassish();
if (_detectClassBasedOnAnnotations(classish, companion)) return;
if (_detectNonLegacyPropsStateOrMixin(classish, companion)) return;
if (_detectPotentialComponent(classish)) return;
}
//
// _classishDeclarationsByName-related utilities
//
/// For `FooProps`, returns `_$FooProps`
NamedCompilationUnitMember? _getSourceClassForPotentialCompanion(
NamedCompilationUnitMember node) {
final name = node.name.name;
if (name.startsWith(privateSourcePrefix)) {
return null;
}
final sourceName = '$privateSourcePrefix$name';
return _classishDeclarationsByName![sourceName];
}
/// For `_$FooProps`, returns `FooProps`
NamedCompilationUnitMember? _getCompanionClass(NamedCompilationUnitMember node) {
final name = node.name.name;
if (!name.startsWith(privateSourcePrefix)) {
return null;
}
final sourceName = name.replaceFirst(privateSourcePrefix, '');
return _classishDeclarationsByName![sourceName];
}
/// Returns whether it's the `$FooPropsMixin` to a `_$FooPropsMixin`
bool _isMixinStub(NamedCompilationUnitMember node) {
final name = node.name.name;
return name.startsWith(r'$') && _classishDeclarationsByName!.containsKey('_$name');
}
//
// _processTopLevelVariableDeclaration helpers
//
void _detectFactory(TopLevelVariableDeclaration node) {
if (node.hasAnnotationWithName('Factory')) {
onFactory(BoilerplateFactory(node, VersionConfidences.all(Confidence.likely)));
return;
}
final rightHandSide = node.variables.firstInitializer;
if (rightHandSide != null && node.usesAGeneratedConfig) {
onFactory(BoilerplateFactory(
node,
VersionConfidences(
v4_mixinBased: Confidence.likely,
v3_legacyDart2Only: Confidence.none,
v2_legacyBackwardsCompat: Confidence.none,
)));
return;
}
final type = node.variables.type;
if (type != null) {
if (type.typeNameWithoutPrefix == 'UiFactory') {
final firstVar = node.variables.variables.first;
final name = firstVar.name.name;
final initializer = firstVar.initializer;
// Check for `Foo = _$Foo` or `Foo = $Foo` (which could be a typo)
final generatedFactoryName = '_\$$name';
final typoGeneratedFactoryName = '\$$name';
final referencesGeneratedFactory = initializer != null &&
anyDescendantIdentifiers(initializer, (identifier) {
return identifier.name == generatedFactoryName ||
identifier.name == typoGeneratedFactoryName;
});
if (referencesGeneratedFactory) {
onFactory(BoilerplateFactory(
node,
VersionConfidences(
v4_mixinBased: Confidence.likely,
v3_legacyDart2Only: Confidence.none,
v2_legacyBackwardsCompat: Confidence.none,
)));
return;
} else {
onFactory(BoilerplateFactory(
node,
VersionConfidences(
v4_mixinBased: Confidence.neutral,
v2_legacyBackwardsCompat: Confidence.none,
v3_legacyDart2Only: Confidence.none,
)));
return;
}
}
}
return;
}
//
// _processClassishDeclaration helpers
//
bool _detectClassBasedOnAnnotations(
ClassishDeclaration classish, ClassishDeclaration? companion) {
final node = classish.node;
for (final annotation in classish.metadata) {
switch (annotation.name.nameWithoutPrefix) {
case 'Props':
// It has never been possible to declare a props class with a mixin, so we can safely
// assume that Dart mixins are not concrete props classes.
//
// Special-case: `@Props()` is allowed on the new boilerplate mixins
if (node is MixinDeclaration) {
onPropsMixin(BoilerplatePropsMixin(
classish,
companion,
_annotatedPropsOrStateMixinConfidence(classish, companion,
disableAnnotationAssert: true)));
} else {
onProps(BoilerplateProps(
classish, companion, _annotatedPropsOrStateConfidence(classish, companion)));
}
return true;
case 'State':
// It has never been possible to declare a state class with a mixin, so we can safely
// assume that Dart mixins are not concrete state classes.
//
// Special-case: `@State()` is allowed on the new boilerplate mixins
if (node is MixinDeclaration) {
onStateMixin(BoilerplateStateMixin(
classish,
companion,
_annotatedPropsOrStateMixinConfidence(classish, companion,
disableAnnotationAssert: true)));
} else {
onState(BoilerplateState(
classish, companion, _annotatedPropsOrStateConfidence(classish, companion)));
}
return true;
case 'PropsMixin':
onPropsMixin(BoilerplatePropsMixin(
classish, companion, _annotatedPropsOrStateMixinConfidence(classish, companion)));
return true;
case 'StateMixin':
onStateMixin(BoilerplateStateMixin(
classish, companion, _annotatedPropsOrStateMixinConfidence(classish, companion)));
return true;
case 'Component':
case 'Component2':
// Don't have lower confidence for mixin-based when `@Component`;
// we want it equal so that it can resolve to mixin-based based on the other parts, and
// warn for not having `@Component2`.
onComponent(BoilerplateComponent(classish, VersionConfidences.all(Confidence.likely)));
return true;
case 'AbstractProps':
onProps(BoilerplateProps(
classish, companion, _annotatedAbstractPropsOrStateConfidence(classish, companion)));
return true;
case 'AbstractState':
onState(BoilerplateState(
classish, companion, _annotatedAbstractPropsOrStateConfidence(classish, companion)));
return true;
case 'AbstractComponent':
case 'AbstractComponent2':
onComponent(BoilerplateComponent(classish, VersionConfidences.none()));
return true;
}
}
return false;
}
VersionConfidences _annotatedPropsOrStateConfidence(
ClassishDeclaration classish, ClassishDeclaration? companion) {
final node = classish.node;
assert(node.hasAnnotationWithNames(const {'Props', 'State'}),
'this function assumes that all nodes passed to this function are annotated');
assert(node is! MixinDeclaration,
'Mixins should never make it in here they should be classified as Props/State mixins');
final hasGeneratedPrefix = node.name.name.startsWith(r'_$');
final hasCompanionClass = companion != null;
if (hasCompanionClass) {
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.likely,
v3_legacyDart2Only: Confidence.unlikely,
v4_mixinBased: Confidence.unlikely,
);
} else if (hasGeneratedPrefix) {
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.unlikely,
v3_legacyDart2Only: Confidence.likely,
v4_mixinBased: Confidence.unlikely,
);
} else {
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.unlikely,
v3_legacyDart2Only: Confidence.unlikely,
v4_mixinBased: Confidence.likely,
);
}
}
VersionConfidences _annotatedAbstractPropsOrStateConfidence(
ClassishDeclaration classish, ClassishDeclaration? companion) {
final node = classish.node;
assert(node.hasAnnotationWithNames(const {'AbstractProps', 'AbstractState'}),
'this function assumes that all nodes passed to this function are annotated');
final hasCompanionClass = companion != null;
if (hasCompanionClass) {
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.likely,
v3_legacyDart2Only: Confidence.unlikely,
// Annotated abstract props/state don't exist to the new boilerplate
v4_mixinBased: Confidence.none,
);
} else {
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.unlikely,
v3_legacyDart2Only: Confidence.likely,
// Annotated abstract props/state don't exist to the new boilerplate
v4_mixinBased: Confidence.none,
);
}
}
VersionConfidences _annotatedPropsOrStateMixinConfidence(
ClassishDeclaration classish, ClassishDeclaration? companion,
{bool disableAnnotationAssert = false}) {
final node = classish.node;
assert(
disableAnnotationAssert || node.hasAnnotationWithNames(const {'PropsMixin', 'StateMixin'}),
'this function assumes that all nodes passed to this function are annotated');
final isMixin = node is MixinDeclaration;
final hasGeneratedPrefix = node.name.name.startsWith(r'_$');
return VersionConfidences(
v2_legacyBackwardsCompat: isMixin
? Confidence.none
: (hasGeneratedPrefix ? Confidence.unlikely : Confidence.likely),
v3_legacyDart2Only: isMixin
? Confidence.none
: (hasGeneratedPrefix ? Confidence.likely : Confidence.unlikely),
v4_mixinBased: isMixin ? Confidence.likely : Confidence.unlikely,
);
}
bool _detectNonLegacyPropsStateOrMixin(
ClassishDeclaration classish, ClassishDeclaration? companion) {
final name = classish.name.name;
final node = classish.node;
// By this point, this is a node that has no annotation.
// Thus, it's non-legacy boilerplate.
VersionConfidences getConfidence() {
// Handle classes that look like props but are really just used as interfaces, and aren't extended from or directly used as a component's props.
// Watch out for empty mixins, though; those are valid props/state mixins.
if (_overridesIsClassGenerated(classish) ||
(node is! MixinDeclaration && onlyImplementsThings(classish))) {
return VersionConfidences.none();
} else if (classish.members.whereType<ConstructorDeclaration>().isNotEmpty) {
// If there's a constructor, it's a no-generate class. For example, a props map view.
return VersionConfidences.none();
}
return VersionConfidences(
v2_legacyBackwardsCompat: Confidence.none,
v3_legacyDart2Only: Confidence.none,
v4_mixinBased: Confidence.likely,
);
}
if (node is MixinDeclaration) {
if (propsMixinNamePattern.hasMatch(name) && node.hasSuperclassConstraint('UiProps')) {
onPropsMixin(BoilerplatePropsMixin(classish, companion, getConfidence()));
return true;
}
if (stateMixinNamePattern.hasMatch(name) && node.hasSuperclassConstraint('UiState')) {
onStateMixin(BoilerplateStateMixin(classish, companion, getConfidence()));
return true;
}
} else {
// We never generate for abstract classes in the new boilerplate.
if (classish.abstractKeyword != null) return false;
final superclassName = classish.superclass?.typeNameWithoutPrefix;
if (propsNamePattern.hasMatch(name) && superclassName == 'UiProps') {
onProps(BoilerplateProps(classish, companion, getConfidence()));
return true;
}
if (stateNamePattern.hasMatch(name) && superclassName == 'UiState') {
onState(BoilerplateState(classish, companion, getConfidence()));
return true;
}
}
return false;
}
/// UiComponent, UiComponent2, UiStatefulComponent, FluxUiComponent, CustomUiComponent, ...
static final _componentBaseClassPattern = RegExp(r'Ui\w*Component');
bool _detectPotentialComponent(ClassishDeclaration classish) {
// Don't detect react-dart components as boilerplate components, since they cause issues with grouping
// if they're in the same file as an OverReact component with non-matching names.
if (!const {'Component', 'Component2'}.contains(classish.superclass?.nameWithoutPrefix)) {
if (classish.name.name.endsWith('Component') ||
classish.allSuperTypes
.map((t) => t.typeNameWithoutPrefix)
.whereNotNull()
.any(_componentBaseClassPattern.hasMatch) ||
(classish.superclass?.typeArguments?.arguments
.map((t) => t.typeNameWithoutPrefix)
.whereNotNull()
.any(propsOrMixinNamePattern.hasMatch) ??
false)) {
const mixinBoilerplateBaseClasses = {
'UiComponent2',
'UiStatefulComponent2',
'FluxUiComponent2',
'FluxUiStatefulComponent2'
};
final confidences = VersionConfidences(
// If the component extends from a base class known to be supported by the new boilerplate,
// has no annotation, is not abstract, and does not have $isClassGenerated, then it's
// most likely intended to be part of a new boilerplate class component declaration.
//
// Make this `likely` so that components that don't get associated with factory/props
// due to naming issues aren't silently ignored.
v4_mixinBased: !classish.hasAbstractKeyword &&
!_overridesIsClassGenerated(classish) &&
mixinBoilerplateBaseClasses.contains(classish.superclass?.nameWithoutPrefix)
? Confidence.likely
: Confidence.neutral,
v2_legacyBackwardsCompat: Confidence.neutral,
v3_legacyDart2Only: Confidence.neutral,
);
onComponent(BoilerplateComponent(classish, confidences));
return true;
}
}
return false;
}
static bool _overridesIsClassGenerated(ClassishDeclaration classish) => classish.members
.whereType<MethodDeclaration>()
.any((member) => member.isGetter && member.name.name == r'$isClassGenerated');
}
class _BoilerplateMemberDetectorVisitor extends SimpleAstVisitor<void> {
final void Function(NamedCompilationUnitMember) onClassishDeclaration;
final void Function(TopLevelVariableDeclaration) onTopLevelVariableDeclaration;
_BoilerplateMemberDetectorVisitor({
required this.onClassishDeclaration,
required this.onTopLevelVariableDeclaration,
});
@override
void visitCompilationUnit(CompilationUnit node) => node.visitChildren(this);
@override
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) =>
onTopLevelVariableDeclaration(node);
@override
void visitClassDeclaration(ClassDeclaration node) => onClassishDeclaration(node);
@override
void visitClassTypeAlias(ClassTypeAlias node) => onClassishDeclaration(node);
@override
void visitMixinDeclaration(MixinDeclaration node) => onClassishDeclaration(node);
}