Skip to content

Commit 9a6fc48

Browse files
stereotype441Commit Queue
authored andcommitted
[linter/analyzer_public_api] Report experimental inconsistencies.
Modifies the `analyzer_public_api` lint so that it reports `analyzer_public_api_experimental_inconsistency` if an element in the analyzer public API that is _not_ marked experimental has a type that refers to an element in the analyzer public API that _is_ marked experimental. Such references are problematic because they can cause a client of the analyzer public API to make use of an experimental type without receiving a warning. Change-Id: I6a6a6964fd5f1d37e163dc27a1cb4e605efd2d76 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/451550 Auto-Submit: Paul Berry <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Paul Berry <[email protected]>
1 parent 1f89a5e commit 9a6fc48

File tree

6 files changed

+517
-59
lines changed

6 files changed

+517
-59
lines changed

pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,8 @@ LintCode.analyzer_public_api_bad_part_directive:
18831883
status: noFix
18841884
LintCode.analyzer_public_api_bad_type:
18851885
status: noFix
1886+
LintCode.analyzer_public_api_experimental_inconsistency:
1887+
status: noFix
18861888
LintCode.analyzer_public_api_exports_non_public_name:
18871889
status: noFix
18881890
LintCode.analyzer_public_api_impl_in_public_api:

pkg/linter/lib/src/lint_codes.g.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,39 @@ class LinterLintCode extends LintCodeWithExpectedTypes {
213213
expectedTypes: [ExpectedType.string],
214214
);
215215

216+
/// Lint issued if an element in the analyzer public API makes use
217+
/// of a type that's annotated `@experimental`, but the element
218+
/// itself is not annotated `@experimental`.
219+
///
220+
/// The reason this is a problem is that it makes it possible for
221+
/// analyzer clients to implicitly reference analyzer experimental
222+
/// types. This can happen in many ways; here are some examples:
223+
///
224+
/// - If `C` is a non-experimental public API class that implements
225+
/// `B`, and `B` is an experimental public API class with a getter
226+
/// called `x`, then a client can access `B.x` via `C`.
227+
///
228+
/// - If `f` has return type `T`, and `T` is an experimental public
229+
/// API class with a getter called `x`, then a client can access
230+
/// `T.x` via `f().x`.
231+
///
232+
/// - If `f` has type `void Function(T)`, and `T` is an experimental
233+
/// public API class with a getter called `x`, then a client can
234+
/// access `T.x` via `var g = f; g = (t) { print(t.x); }`.
235+
///
236+
/// Parameters:
237+
/// String types: list of types, separated by `, `
238+
static const LinterLintTemplate<
239+
LocatableDiagnostic Function({required String types})
240+
>
241+
analyzerPublicApiExperimentalInconsistency = LinterLintTemplate(
242+
LintNames.analyzer_public_api_experimental_inconsistency,
243+
"Element makes use of experimental type(s), but is not itself marked with "
244+
"`@experimental`: {0}.",
245+
withArguments: _withArgumentsAnalyzerPublicApiExperimentalInconsistency,
246+
expectedTypes: [ExpectedType.string],
247+
);
248+
216249
/// Lint issued if a file in the analyzer public API contains an `export`
217250
/// directive that exports a name that's not part of the analyzer public API.
218251
///
@@ -3407,6 +3440,15 @@ class LinterLintCode extends LintCodeWithExpectedTypes {
34073440
return LocatableDiagnosticImpl(analyzerPublicApiBadType, [types]);
34083441
}
34093442

3443+
static LocatableDiagnostic
3444+
_withArgumentsAnalyzerPublicApiExperimentalInconsistency({
3445+
required String types,
3446+
}) {
3447+
return LocatableDiagnosticImpl(analyzerPublicApiExperimentalInconsistency, [
3448+
types,
3449+
]);
3450+
}
3451+
34103452
static LocatableDiagnostic
34113453
_withArgumentsAnalyzerPublicApiExportsNonPublicName({
34123454
required String elements,

pkg/linter/lib/src/lint_names.g.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ abstract final class LintNames {
4444
static const String analyzer_public_api_bad_type =
4545
'analyzer_public_api_bad_type';
4646

47+
static const String analyzer_public_api_experimental_inconsistency =
48+
'analyzer_public_api_experimental_inconsistency';
49+
4750
static const String analyzer_public_api_exports_non_public_name =
4851
'analyzer_public_api_exports_non_public_name';
4952

pkg/linter/lib/src/rules/analyzer_public_api.dart

Lines changed: 152 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class AnalyzerPublicApi extends MultiAnalysisRule {
3232
List<DiagnosticCode> get diagnosticCodes => [
3333
LinterLintCode.analyzerPublicApiBadPartDirective,
3434
LinterLintCode.analyzerPublicApiBadType,
35+
LinterLintCode.analyzerPublicApiExperimentalInconsistency,
3536
LinterLintCode.analyzerPublicApiExportsNonPublicName,
3637
LinterLintCode.analyzerPublicApiImplInPublicApi,
3738
];
@@ -48,6 +49,16 @@ class AnalyzerPublicApi extends MultiAnalysisRule {
4849
}
4950
}
5051

52+
/// Types of problematic uses of types that can be detected by this lint.
53+
enum _ProblematicTypeUseKind {
54+
/// Use of a non-public type by a part of the analyzer public API.
55+
useOfNonPublicType,
56+
57+
/// Use of an experimental type by a non-experimental part of the analyzer
58+
/// public API.
59+
useOfExperimentalType,
60+
}
61+
5162
/// Lightweight object representing a public import.
5263
class _PublicImport {
5364
final Element? Function(String name) lookup;
@@ -130,7 +141,7 @@ class _Visitor extends SimpleAstVisitor<void> {
130141
}
131142
}
132143

133-
void _checkMember(Fragment fragment) {
144+
void _checkMember(Fragment fragment, {required bool isParentExperimental}) {
134145
if (fragment is ConstructorFragment &&
135146
fragment.element.enclosingElement is EnumElement) {
136147
// Enum constructors aren't callable from outside of the enum, so they
@@ -143,7 +154,12 @@ class _Visitor extends SimpleAstVisitor<void> {
143154
return;
144155
}
145156
if (fragment is ExecutableFragment) {
146-
_checkType(fragment.element.type, fragment: fragment);
157+
_checkType(
158+
fragment.element.type,
159+
fragment: fragment,
160+
isUsageSiteExperimental:
161+
isParentExperimental || fragment.element.metadata.hasExperimental,
162+
);
147163
}
148164
}
149165

@@ -158,95 +174,136 @@ class _Visitor extends SimpleAstVisitor<void> {
158174
diagnosticCode: LinterLintCode.analyzerPublicApiImplInPublicApi,
159175
);
160176
}
177+
var isUsageSiteExperimental = fragment.element.metadata.hasExperimental;
161178
switch (fragment) {
162179
case ExtensionFragment(name: null):
163180
// Unnamed extensions are not public, so ignore.
164181
break;
165182
case InstanceFragment(:var typeParameters, :var children):
166183
for (var typeParameter in typeParameters) {
167-
_checkTypeParameter(typeParameter, fragment: fragment);
184+
_checkTypeParameter(
185+
typeParameter,
186+
fragment: fragment,
187+
isUsageSiteExperimental: isUsageSiteExperimental,
188+
);
168189
}
169190
if (fragment is InterfaceFragment) {
170191
var element = fragment.element;
171-
_checkType(element.supertype, fragment: fragment);
192+
_checkType(
193+
element.supertype,
194+
fragment: fragment,
195+
isUsageSiteExperimental: isUsageSiteExperimental,
196+
);
172197
for (var t in element.interfaces) {
173-
_checkType(t, fragment: fragment);
198+
_checkType(
199+
t,
200+
fragment: fragment,
201+
isUsageSiteExperimental: isUsageSiteExperimental,
202+
);
174203
}
175204
for (var t in element.mixins) {
176-
_checkType(t, fragment: fragment);
205+
_checkType(
206+
t,
207+
fragment: fragment,
208+
isUsageSiteExperimental: isUsageSiteExperimental,
209+
);
177210
}
178211
}
179212
if (fragment case ExtensionFragment(:var element)) {
180-
_checkType(element.extendedType, fragment: fragment);
213+
_checkType(
214+
element.extendedType,
215+
fragment: fragment,
216+
isUsageSiteExperimental: isUsageSiteExperimental,
217+
);
181218
}
182219
if (fragment case MixinFragment(:var superclassConstraints)) {
183220
for (var t in superclassConstraints) {
184-
_checkType(t, fragment: fragment);
221+
_checkType(
222+
t,
223+
fragment: fragment,
224+
isUsageSiteExperimental: isUsageSiteExperimental,
225+
);
185226
}
186227
}
187-
children.forEach(_checkMember);
228+
for (var child in children) {
229+
_checkMember(child, isParentExperimental: isUsageSiteExperimental);
230+
}
188231
case ExecutableFragment():
189-
_checkType(fragment.element.type, fragment: fragment);
232+
_checkType(
233+
fragment.element.type,
234+
fragment: fragment,
235+
isUsageSiteExperimental: isUsageSiteExperimental,
236+
);
190237
case TypeAliasFragment(:var element, :var typeParameters):
191238
var aliasedType = element.aliasedType;
192-
_checkType(aliasedType, fragment: fragment);
239+
_checkType(
240+
aliasedType,
241+
fragment: fragment,
242+
isUsageSiteExperimental: isUsageSiteExperimental,
243+
);
193244
if (typeParameters.isNotEmpty &&
194245
aliasedType is FunctionType &&
195246
aliasedType.typeParameters.isEmpty) {
196247
// Sometimes `aliasedType` doesn't have the type parameters. Not sure
197248
// why.
198249
// TODO(paulberry): consider fixing this in the analyzer.
199250
for (var typeParameter in typeParameters) {
200-
_checkTypeParameter(typeParameter, fragment: fragment);
251+
_checkTypeParameter(
252+
typeParameter,
253+
fragment: fragment,
254+
isUsageSiteExperimental: isUsageSiteExperimental,
255+
);
201256
}
202257
}
203258
}
204259
}
205260

206-
void _checkType(DartType? type, {required Fragment fragment}) {
261+
void _checkType(
262+
DartType? type, {
263+
required Fragment fragment,
264+
required bool isUsageSiteExperimental,
265+
}) {
207266
if (type == null) return;
208-
var problems = _problemsForAnalyzerPublicApi(type);
209-
if (problems.isEmpty) return;
210-
int offset;
211-
int length;
212-
while (true) {
213-
if (fragment.nameOffset != null) {
214-
offset = fragment.nameOffset!;
215-
length = fragment.name!.length;
216-
break;
217-
} else if (fragment case PropertyAccessorFragment()
218-
when fragment.element.variable.firstFragment.nameOffset != null) {
219-
offset = fragment.element.variable.firstFragment.nameOffset!;
220-
length = fragment.element.variable.name!.length;
221-
break;
222-
} else if (fragment is ConstructorFragment &&
223-
fragment.typeNameOffset != null) {
224-
offset = fragment.typeNameOffset!;
225-
length = fragment.enclosingFragment!.name!.length;
226-
break;
227-
} else if (fragment.enclosingFragment case var enclosingFragment?) {
228-
fragment = enclosingFragment;
229-
} else {
230-
// This should never happen. But if it does, make sure we generate a
231-
// lint anyway.
232-
offset = 0;
233-
length = 1;
234-
break;
235-
}
236-
}
237-
rule.reportAtOffset(
238-
offset,
239-
length,
240-
diagnosticCode: LinterLintCode.analyzerPublicApiBadType,
241-
arguments: [problems.join(', ')],
267+
var nonPublicProblems = _problemsForAnalyzerPublicApi(
268+
type,
269+
kind: _ProblematicTypeUseKind.useOfNonPublicType,
242270
);
271+
var experimentalProblems = isUsageSiteExperimental
272+
? const <String>{}
273+
: _problemsForAnalyzerPublicApi(
274+
type,
275+
kind: _ProblematicTypeUseKind.useOfExperimentalType,
276+
);
277+
late var offsetAndLength = _offsetAndLengthForFragment(fragment);
278+
if (nonPublicProblems.isNotEmpty) {
279+
rule.reportAtOffset(
280+
offsetAndLength.offset,
281+
offsetAndLength.length,
282+
diagnosticCode: LinterLintCode.analyzerPublicApiBadType,
283+
arguments: [nonPublicProblems.join(', ')],
284+
);
285+
}
286+
if (experimentalProblems.isNotEmpty) {
287+
rule.reportAtOffset(
288+
offsetAndLength.offset,
289+
offsetAndLength.length,
290+
diagnosticCode:
291+
LinterLintCode.analyzerPublicApiExperimentalInconsistency,
292+
arguments: [nonPublicProblems.join(', ')],
293+
);
294+
}
243295
}
244296

245297
void _checkTypeParameter(
246298
TypeParameterFragment typeParameter, {
247299
required Fragment fragment,
300+
required bool isUsageSiteExperimental,
248301
}) {
249-
_checkType(typeParameter.element.bound, fragment: fragment);
302+
_checkType(
303+
typeParameter.element.bound,
304+
fragment: fragment,
305+
isUsageSiteExperimental: isUsageSiteExperimental,
306+
);
250307
}
251308

252309
/// Whether [element] is visible through at least one public import.
@@ -270,20 +327,56 @@ class _Visitor extends SimpleAstVisitor<void> {
270327
return false;
271328
}
272329

273-
Set<String> _problemsForAnalyzerPublicApi(DartType type) {
330+
({int length, int offset}) _offsetAndLengthForFragment(Fragment fragment) {
331+
while (true) {
332+
if (fragment.nameOffset != null) {
333+
return (offset: fragment.nameOffset!, length: fragment.name!.length);
334+
} else if (fragment case PropertyAccessorFragment()
335+
when fragment.element.variable.firstFragment.nameOffset != null) {
336+
return (
337+
offset: fragment.element.variable.firstFragment.nameOffset!,
338+
length: fragment.element.variable.name!.length,
339+
);
340+
} else if (fragment is ConstructorFragment &&
341+
fragment.typeNameOffset != null) {
342+
return (
343+
offset: fragment.typeNameOffset!,
344+
length: fragment.enclosingFragment!.name!.length,
345+
);
346+
} else if (fragment.enclosingFragment case var enclosingFragment?) {
347+
// ignore: parameter_assignments
348+
fragment = enclosingFragment;
349+
} else {
350+
// This should never happen. But if it does, make sure we generate a
351+
// lint anyway.
352+
return (offset: 0, length: 1);
353+
}
354+
}
355+
}
356+
357+
Set<String> _problemsForAnalyzerPublicApi(
358+
DartType type, {
359+
required _ProblematicTypeUseKind kind,
360+
}) {
274361
switch (type) {
275362
case RecordType(:var positionalFields, :var namedFields):
276363
return {
277364
for (var f in positionalFields)
278-
..._problemsForAnalyzerPublicApi(f.type),
279-
for (var f in namedFields) ..._problemsForAnalyzerPublicApi(f.type),
365+
..._problemsForAnalyzerPublicApi(f.type, kind: kind),
366+
for (var f in namedFields)
367+
..._problemsForAnalyzerPublicApi(f.type, kind: kind),
280368
};
281369
case InterfaceType(:var element, :var typeArguments):
370+
var isProblematicElement = switch (kind) {
371+
_ProblematicTypeUseKind.useOfNonPublicType =>
372+
!_isPubliclyImported(element) && !element.isOkForAnalyzerPublicApi,
373+
_ProblematicTypeUseKind.useOfExperimentalType =>
374+
element.metadata.hasExperimental,
375+
};
282376
return {
283-
if (!_isPubliclyImported(element) &&
284-
!element.isOkForAnalyzerPublicApi)
285-
element.name!,
286-
for (var t in typeArguments) ..._problemsForAnalyzerPublicApi(t),
377+
if (isProblematicElement) element.name!,
378+
for (var t in typeArguments)
379+
..._problemsForAnalyzerPublicApi(t, kind: kind),
287380
};
288381
case NeverType():
289382
case DynamicType():
@@ -297,11 +390,12 @@ class _Visitor extends SimpleAstVisitor<void> {
297390
:var formalParameters,
298391
):
299392
return {
300-
..._problemsForAnalyzerPublicApi(returnType),
393+
..._problemsForAnalyzerPublicApi(returnType, kind: kind),
301394
for (var p in typeParameters)
302-
if (p.bound != null) ..._problemsForAnalyzerPublicApi(p.bound!),
395+
if (p.bound != null)
396+
..._problemsForAnalyzerPublicApi(p.bound!, kind: kind),
303397
for (var p in formalParameters)
304-
..._problemsForAnalyzerPublicApi(p.type),
398+
..._problemsForAnalyzerPublicApi(p.type, kind: kind),
305399
};
306400
default:
307401
throw StateError('Unexpected type $runtimeType');

0 commit comments

Comments
 (0)