Skip to content

Commit 1462909

Browse files
srawlinscommit-bot@chromium.org
authored andcommitted
Enforce @VisibleForTesting annotations.
In particular, enforce that a method annotated with @VisibleForTesting can only be referenced from within the declaring library, or within a file that has "/test" in its path. This allows @VisibleForTesting methods to be accessed from test files, or files in "/testing" folders, etc. Bug: #28273 Change-Id: I3c19f73be330c49face7e3203879742d514bca74 Reviewed-on: https://dart-review.googlesource.com/27201 Commit-Queue: Samuel Rawlins <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent 3be583a commit 1462909

File tree

8 files changed

+235
-17
lines changed

8 files changed

+235
-17
lines changed

pkg/analyzer/lib/dart/element/element.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,10 @@ abstract class Element implements AnalysisTarget, ResolutionTarget {
665665
*/
666666
bool get isSynthetic;
667667

668+
/// Return `true` if this element has an annotation of the form
669+
/// '@visibleForTesting'.
670+
bool get isVisibleForTesting;
671+
668672
/**
669673
* Return the kind of element that this is.
670674
*/
@@ -873,6 +877,10 @@ abstract class ElementAnnotation
873877
*/
874878
bool get isRequired;
875879

880+
/// Return `true` if this annotation marks the associated member as being
881+
/// visible for testing.
882+
bool get isVisibleForTesting;
883+
876884
/**
877885
* Return a representation of the value of this annotation, forcing the value
878886
* to be computed if it had not previously been computed, or `null` if the

pkg/analyzer/lib/error/error.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ const List<ErrorCode> errorCodeValues = const [
261261
HintCode.INVALID_METHOD_OVERRIDE_TYPE_PARAMETER_BOUND,
262262
HintCode.INVALID_REQUIRED_PARAM,
263263
HintCode.INVALID_USE_OF_PROTECTED_MEMBER,
264+
HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER,
264265
HintCode.IS_DOUBLE,
265266
HintCode.IS_INT,
266267
HintCode.IS_NOT_DOUBLE,

pkg/analyzer/lib/src/dart/element/element.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,6 +2874,10 @@ class ElementAnnotationImpl implements ElementAnnotation {
28742874
*/
28752875
static String _REQUIRED_VARIABLE_NAME = "required";
28762876

2877+
/// The name of the top-level variable used to mark a method as being
2878+
/// visible for testing.
2879+
static String _VISIBLE_FOR_TESTING_VARIABLE_NAME = "visibleForTesting";
2880+
28772881
/**
28782882
* The element representing the field, variable, or constructor being used as
28792883
* an annotation.
@@ -2988,6 +2992,12 @@ class ElementAnnotationImpl implements ElementAnnotation {
29882992
element.name == _REQUIRED_VARIABLE_NAME &&
29892993
element.library?.name == _META_LIB_NAME;
29902994

2995+
@override
2996+
bool get isVisibleForTesting =>
2997+
element is PropertyAccessorElement &&
2998+
element.name == _VISIBLE_FOR_TESTING_VARIABLE_NAME &&
2999+
element.library?.name == _META_LIB_NAME;
3000+
29913001
/**
29923002
* Get the library containing this annotation.
29933003
*/
@@ -3259,6 +3269,10 @@ abstract class ElementImpl implements Element {
32593269
setModifier(Modifier.SYNTHETIC, isSynthetic);
32603270
}
32613271

3272+
@override
3273+
bool get isVisibleForTesting => metadata
3274+
.any((ElementAnnotation annotation) => annotation.isVisibleForTesting);
3275+
32623276
@override
32633277
LibraryElement get library =>
32643278
getAncestor((element) => element is LibraryElement);
@@ -7464,6 +7478,9 @@ class MultiplyDefinedElementImpl implements MultiplyDefinedElement {
74647478
@override
74657479
bool get isSynthetic => true;
74667480

7481+
@override
7482+
bool get isVisibleForTesting => false;
7483+
74677484
@override
74687485
ElementKind get kind => ElementKind.ERROR;
74697486

pkg/analyzer/lib/src/dart/element/handle.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ abstract class ElementHandle implements Element {
377377
@override
378378
bool get isSynthetic => actualElement.isSynthetic;
379379

380+
@override
381+
bool get isVisibleForTesting => actualElement.isVisibleForTesting;
382+
380383
@override
381384
LibraryElement get library =>
382385
getAncestor((element) => element is LibraryElement);

pkg/analyzer/lib/src/dart/element/member.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ abstract class Member implements Element {
423423
@override
424424
bool get isSynthetic => _baseElement.isSynthetic;
425425

426+
@override
427+
bool get isVisibleForTesting => _baseElement.isVisibleForTesting;
428+
426429
@override
427430
ElementKind get kind => _baseElement.kind;
428431

pkg/analyzer/lib/src/dart/error/hint_codes.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,16 @@ class HintCode extends ErrorCode {
303303
"The member '{0}' can only be used within instance members of subclasses "
304304
"of '{1}'.");
305305

306+
/// This hint is generated anywhere where a member annotated with
307+
/// `@visibleForTesting` is used outside the defining library, or a test.
308+
///
309+
/// Parameters:
310+
/// 0: the name of the member
311+
/// 1: the name of the defining class
312+
static const HintCode INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER =
313+
const HintCode('INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER',
314+
"The member '{0}' can only be used within '{1}' or a test.");
315+
306316
/**
307317
* Hint for the `x is double` type checks.
308318
*/

pkg/analyzer/lib/src/generated/resolver.dart

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import 'package:analyzer/src/generated/static_type_analyzer.dart';
3232
import 'package:analyzer/src/generated/testing/element_factory.dart';
3333
import 'package:analyzer/src/generated/type_system.dart';
3434
import 'package:analyzer/src/generated/utilities_dart.dart';
35+
import 'package:path/path.dart' as path;
3536

3637
export 'package:analyzer/src/dart/resolver/inheritance_manager.dart';
3738
export 'package:analyzer/src/dart/resolver/scope.dart';
@@ -48,6 +49,8 @@ class BestPracticesVerifier extends RecursiveAstVisitor<Object> {
4849

4950
static String _TO_INT_METHOD_NAME = "toInt";
5051

52+
static final _testDir = '${path.separator}test${path.separator}';
53+
5154
/**
5255
* The class containing the AST nodes being visited, or `null` if we are not in the scope of
5356
* a class.
@@ -353,7 +356,7 @@ class BestPracticesVerifier extends RecursiveAstVisitor<Object> {
353356
@override
354357
Object visitSimpleIdentifier(SimpleIdentifier node) {
355358
_checkForDeprecatedMemberUseAtIdentifier(node);
356-
_checkForInvalidProtectedMemberAccess(node);
359+
_checkForInvalidAccess(node);
357360
return super.visitSimpleIdentifier(node);
358361
}
359362

@@ -902,11 +905,17 @@ class BestPracticesVerifier extends RecursiveAstVisitor<Object> {
902905
decl.name, [decl.name.toString()]);
903906
}
904907

905-
/**
906-
* Produces a hint if the given identifier is a protected closure, field or
907-
* getter/setter, method closure or invocation accessed outside a subclass.
908-
*/
909-
void _checkForInvalidProtectedMemberAccess(SimpleIdentifier identifier) {
908+
/// Produces a hint if [identifier] is accessed from an invalid location. In
909+
/// particular:
910+
///
911+
/// * if the given identifier is a protected closure, field or
912+
/// getter/setter, method closure or invocation accessed outside a subclass,
913+
/// or accessed outside the library wherein the identifier is declared, or
914+
/// * if the given identifier is a closure, field, getter, setter, method
915+
/// closure or invocation which is annotated with `visibleForTesting`, and
916+
/// is accessed outside of the defining library, and the current library
917+
/// does not have the word 'test' in its name.
918+
void _checkForInvalidAccess(SimpleIdentifier identifier) {
910919
if (identifier.inDeclarationContext()) {
911920
return;
912921
}
@@ -925,28 +934,60 @@ class BestPracticesVerifier extends RecursiveAstVisitor<Object> {
925934
return false;
926935
}
927936

937+
bool isVisibleForTesting(Element element) {
938+
if (element == null) {
939+
return false;
940+
}
941+
if (element.isVisibleForTesting) {
942+
return true;
943+
}
944+
if (element is PropertyAccessorElement &&
945+
element.enclosingElement is ClassElement &&
946+
element.variable.isVisibleForTesting) {
947+
return true;
948+
}
949+
return false;
950+
}
951+
928952
bool inCommentReference(SimpleIdentifier identifier) =>
929953
identifier.getAncestor((AstNode node) => node is CommentReference) !=
930954
null;
931955

932956
bool inCurrentLibrary(Element element) =>
933957
element.library == _currentLibrary;
934958

959+
bool inTestDirectory(LibraryElement library) =>
960+
library.definingCompilationUnit.source.fullName.contains(_testDir);
961+
935962
Element element = identifier.bestElement;
936-
if (isProtected(element) &&
937-
!inCurrentLibrary(element) &&
938-
!inCommentReference(identifier)) {
963+
if (isProtected(element)) {
964+
if (inCurrentLibrary(element) || inCommentReference(identifier)) {
965+
// The access is valid; even if [element] is also marked
966+
// `visibleForTesting`, the "visibilities" are unioned.
967+
return;
968+
}
939969
ClassElement definingClass = element.enclosingElement;
940970
ClassDeclaration accessingClass =
941971
identifier.getAncestor((AstNode node) => node is ClassDeclaration);
942-
if (accessingClass == null ||
943-
!_hasTypeOrSuperType(accessingClass.element, definingClass.type)) {
972+
if (_hasTypeOrSuperType(accessingClass?.element, definingClass.type)) {
973+
return;
974+
} else {
944975
_errorReporter.reportErrorForNode(
945976
HintCode.INVALID_USE_OF_PROTECTED_MEMBER,
946977
identifier,
947978
[identifier.name.toString(), definingClass.name]);
948979
}
949980
}
981+
if (isVisibleForTesting(element) &&
982+
!inCurrentLibrary(element) &&
983+
!inTestDirectory(_currentLibrary) &&
984+
!inCommentReference(identifier)) {
985+
Element definingClass = element.enclosingElement;
986+
_errorReporter.reportErrorForNode(
987+
HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER,
988+
identifier,
989+
[identifier.name.toString(), definingClass.name]);
990+
}
950991
}
951992

952993
/**

pkg/analyzer/test/generated/hint_code_test.dart

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ const _Literal literal = const _Literal();
3838
const _MustCallSuper mustCallSuper = const _MustCallSuper();
3939
const _Protected protected = const _Protected();
4040
const Required required = const Required();
41-
class Required {
42-
final String reason;
43-
const Required([this.reason]);
44-
}
41+
const _VisibleForTesting visibleForTesting = const _VisibleForTesting();
4542
4643
class Immutable {
4744
final String reason;
@@ -62,9 +59,12 @@ class _MustCallSuper {
6259
class _Protected {
6360
const _Protected();
6461
}
65-
class _Required {
62+
class Required {
6663
final String reason;
67-
const _Required([this.reason]);
64+
const Required([this.reason]);
65+
}
66+
class _VisibleForTesting {
67+
const _VisibleForTesting();
6868
}
6969
'''
7070
],
@@ -1923,6 +1923,141 @@ main() {
19231923
verify([source]);
19241924
}
19251925

1926+
test_invalidUseOfVisibleForTestingMember_method() async {
1927+
Source source = addNamedSource('/lib1.dart', r'''
1928+
import 'package:meta/meta.dart';
1929+
class A {
1930+
@visibleForTesting
1931+
void a(){ }
1932+
}
1933+
''');
1934+
Source source2 = addNamedSource('/lib2.dart', r'''
1935+
import 'lib1.dart';
1936+
1937+
class B {
1938+
void b() => new A().a();
1939+
}
1940+
''');
1941+
await computeAnalysisResult(source);
1942+
await computeAnalysisResult(source2);
1943+
assertErrors(source2, [HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER]);
1944+
verify([source, source2]);
1945+
}
1946+
1947+
test_invalidUseOfVisibleForTestingMember_method_OK() async {
1948+
Source source = addNamedSource('/lib1.dart', r'''
1949+
import 'package:meta/meta.dart';
1950+
class A {
1951+
@visibleForTesting
1952+
void a(){ }
1953+
}
1954+
''');
1955+
Source source2 = addNamedSource('/test/test1.dart', r'''
1956+
import '../lib1.dart';
1957+
1958+
class B {
1959+
void b() => new A().a();
1960+
}
1961+
''');
1962+
await computeAnalysisResult(source);
1963+
await computeAnalysisResult(source2);
1964+
assertNoErrors(source2);
1965+
verify([source, source2]);
1966+
}
1967+
1968+
test_invalidUseProtectedAndForTesting_method_OK() async {
1969+
Source source = addNamedSource('/lib1.dart', r'''
1970+
import 'package:meta/meta.dart';
1971+
class A {
1972+
@protected
1973+
@visibleForTesting
1974+
void a(){ }
1975+
}
1976+
''');
1977+
Source source2 = addNamedSource('/lib2.dart', r'''
1978+
import 'lib1.dart';
1979+
1980+
class B extends A {
1981+
void b() => new A().a();
1982+
}
1983+
''');
1984+
await computeAnalysisResult(source);
1985+
await computeAnalysisResult(source2);
1986+
assertNoErrors(source2);
1987+
verify([source, source2]);
1988+
}
1989+
1990+
test_invalidUseOfVisibleForTestingMember_propertyAccess() async {
1991+
Source source = addNamedSource('/lib1.dart', r'''
1992+
import 'package:meta/meta.dart';
1993+
class A {
1994+
@visibleForTesting
1995+
int get a => 7;
1996+
1997+
@visibleForTesting
1998+
set b(_) => 7;
1999+
}
2000+
''');
2001+
Source source2 = addNamedSource('/lib2.dart', r'''
2002+
import 'lib1.dart';
2003+
2004+
void main() {
2005+
new A().a;
2006+
new A().b = 6;
2007+
}
2008+
''');
2009+
await computeAnalysisResult(source);
2010+
await computeAnalysisResult(source2);
2011+
assertErrors(source2, [
2012+
HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER,
2013+
HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER
2014+
]);
2015+
verify([source, source2]);
2016+
}
2017+
2018+
test_invalidUseOfVisibleForTestingMember_constructor() async {
2019+
Source source = addNamedSource('/lib1.dart', r'''
2020+
import 'package:meta/meta.dart';
2021+
class A {
2022+
int _x;
2023+
2024+
@visibleForTesting
2025+
A.forTesting(this._x);
2026+
}
2027+
''');
2028+
Source source2 = addNamedSource('/lib2.dart', r'''
2029+
import 'lib1.dart';
2030+
2031+
void main() {
2032+
new A.forTesting(0);
2033+
}
2034+
''');
2035+
await computeAnalysisResult(source);
2036+
await computeAnalysisResult(source2);
2037+
assertErrors(source2, [HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER]);
2038+
verify([source, source2]);
2039+
}
2040+
2041+
test_invalidUseOfVisibleForTestingMember_topLevelFunction() async {
2042+
Source source = addNamedSource('/lib1.dart', r'''
2043+
import 'package:meta/meta.dart';
2044+
2045+
@visibleForTesting
2046+
int fn0() => 1;
2047+
''');
2048+
Source source2 = addNamedSource('/lib2.dart', r'''
2049+
import 'lib1.dart';
2050+
2051+
void main() {
2052+
fn0();
2053+
}
2054+
''');
2055+
await computeAnalysisResult(source);
2056+
await computeAnalysisResult(source2);
2057+
assertErrors(source2, [HintCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER]);
2058+
verify([source, source2]);
2059+
}
2060+
19262061
test_isDouble() async {
19272062
AnalysisOptionsImpl options = new AnalysisOptionsImpl();
19282063
options.dart2jsHint = true;

0 commit comments

Comments
 (0)