Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1017)

Side by Side Diff: pkg/kernel/lib/verifier.dart

Issue 2620133004: Report problems through a method. (Closed)
Patch Set: Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 library kernel.checks; 4 library kernel.checks;
5 5
6 import 'ast.dart'; 6 import 'ast.dart';
7 import 'transformations/flags.dart'; 7 import 'transformations/flags.dart';
8 8
9 void verifyProgram(Program program) { 9 void verifyProgram(Program program) {
10 VerifyingVisitor.check(program); 10 VerifyingVisitor.check(program);
11 } 11 }
12 12
13 class VerificationError {
14 final TreeNode context;
15
16 final TreeNode node;
17
18 final String details;
19
20 VerificationError(this.context, this.node, this.details);
21
22 toString() {
23 Location location;
24 try {
25 location = node?.location ?? context?.location;
26 } catch (_) {
27 // TODO(ahe): Fix the compiler instead.
28 }
29 if (location != null) {
30 String file = location.file ?? "";
31 return "$file:${location.line}:${location.column}: Verification error:"
32 " $details";
33 } else {
34 return
35 "Verification error: $details\nContext: '$context'.\nNode: '$node'.";
36 }
37 }
38 }
39
13 /// Checks that a kernel program is well-formed. 40 /// Checks that a kernel program is well-formed.
14 /// 41 ///
15 /// This does not include any kind of type checking. 42 /// This does not include any kind of type checking.
16 class VerifyingVisitor extends RecursiveVisitor { 43 class VerifyingVisitor extends RecursiveVisitor {
17 final Set<Class> classes = new Set<Class>(); 44 final Set<Class> classes = new Set<Class>();
18 final Set<TypeParameter> typeParameters = new Set<TypeParameter>(); 45 final Set<TypeParameter> typeParameters = new Set<TypeParameter>();
19 final List<VariableDeclaration> variableStack = <VariableDeclaration>[]; 46 final List<VariableDeclaration> variableStack = <VariableDeclaration>[];
20 bool classTypeParametersAreInScope = false; 47 bool classTypeParametersAreInScope = false;
21 48
22 /// If true, relax certain checks for *outline* mode. For example, don't 49 /// If true, relax certain checks for *outline* mode. For example, don't
23 /// attempt to validate constructor initializers. 50 /// attempt to validate constructor initializers.
24 bool isOutline = false; 51 bool isOutline = false;
25 52
26 Member currentMember; 53 Member currentMember;
27 Class currentClass; 54 Class currentClass;
28 TreeNode currentParent; 55 TreeNode currentParent;
29 56
30 TreeNode get context => currentMember ?? currentClass; 57 TreeNode get context => currentMember ?? currentClass;
31 58
32 static void check(Program program) { 59 static void check(Program program) {
33 program.accept(new VerifyingVisitor()); 60 program.accept(new VerifyingVisitor());
34 } 61 }
35 62
36 defaultTreeNode(TreeNode node) { 63 defaultTreeNode(TreeNode node) {
37 visitChildren(node); 64 visitChildren(node);
38 } 65 }
39 66
67 problem(TreeNode node, String details) {
68 throw new VerificationError(context, node, details);
69 }
70
40 TreeNode enterParent(TreeNode node) { 71 TreeNode enterParent(TreeNode node) {
41 if (!identical(node.parent, currentParent)) { 72 if (!identical(node.parent, currentParent)) {
42 throw 'Incorrect parent pointer on ${node.runtimeType} in $context. ' 73 problem(node,
43 'Parent pointer is ${node.parent.runtimeType}, ' 74 "Incorrect parent pointer: expected '${node.parent.runtimeType}',"
44 'actual parent is ${currentParent.runtimeType}.'; 75 " but found: '${currentParent.runtimeType}'.");
45 } 76 }
46 var oldParent = currentParent; 77 var oldParent = currentParent;
47 currentParent = node; 78 currentParent = node;
48 return oldParent; 79 return oldParent;
49 } 80 }
50 81
51 void exitParent(TreeNode oldParent) { 82 void exitParent(TreeNode oldParent) {
52 currentParent = oldParent; 83 currentParent = oldParent;
53 } 84 }
54 85
(...skipping 13 matching lines...) Expand all
68 } 99 }
69 100
70 void visitWithLocalScope(TreeNode node) { 101 void visitWithLocalScope(TreeNode node) {
71 int stackHeight = enterLocalScope(); 102 int stackHeight = enterLocalScope();
72 visitChildren(node); 103 visitChildren(node);
73 exitLocalScope(stackHeight); 104 exitLocalScope(stackHeight);
74 } 105 }
75 106
76 void declareMember(Member member) { 107 void declareMember(Member member) {
77 if (member.transformerFlags & TransformerFlag.seenByVerifier != 0) { 108 if (member.transformerFlags & TransformerFlag.seenByVerifier != 0) {
78 throw '$member has been declared more than once (${member.location})'; 109 problem(member.function,
110 "Member '$member' has been declared more than once.");
79 } 111 }
80 member.transformerFlags |= TransformerFlag.seenByVerifier; 112 member.transformerFlags |= TransformerFlag.seenByVerifier;
81 } 113 }
82 114
83 void undeclareMember(Member member) { 115 void undeclareMember(Member member) {
84 member.transformerFlags &= ~TransformerFlag.seenByVerifier; 116 member.transformerFlags &= ~TransformerFlag.seenByVerifier;
85 } 117 }
86 118
87 void declareVariable(VariableDeclaration variable) { 119 void declareVariable(VariableDeclaration variable) {
88 if (variable.flags & VariableDeclaration.FlagInScope != 0) { 120 if (variable.flags & VariableDeclaration.FlagInScope != 0) {
89 throw '$variable declared more than once (${variable.location})'; 121 problem(variable, "Variable '$variable' declared more than once.");
90 } 122 }
91 variable.flags |= VariableDeclaration.FlagInScope; 123 variable.flags |= VariableDeclaration.FlagInScope;
92 variableStack.add(variable); 124 variableStack.add(variable);
93 } 125 }
94 126
95 void undeclareVariable(VariableDeclaration variable) { 127 void undeclareVariable(VariableDeclaration variable) {
96 variable.flags &= ~VariableDeclaration.FlagInScope; 128 variable.flags &= ~VariableDeclaration.FlagInScope;
97 } 129 }
98 130
99 void declareTypeParameters(List<TypeParameter> parameters) { 131 void declareTypeParameters(List<TypeParameter> parameters) {
100 for (int i = 0; i < parameters.length; ++i) { 132 for (int i = 0; i < parameters.length; ++i) {
101 var parameter = parameters[i]; 133 var parameter = parameters[i];
102 if (!typeParameters.add(parameter)) { 134 if (!typeParameters.add(parameter)) {
103 throw 'Type parameter $parameter redeclared in $context'; 135 problem(parameter, "Type parameter '$parameter' redeclared.");
104 } 136 }
105 } 137 }
106 } 138 }
107 139
108 void undeclareTypeParameters(List<TypeParameter> parameters) { 140 void undeclareTypeParameters(List<TypeParameter> parameters) {
109 typeParameters.removeAll(parameters); 141 typeParameters.removeAll(parameters);
110 } 142 }
111 143
112 void checkVariableInScope(VariableDeclaration variable, TreeNode where) { 144 void checkVariableInScope(VariableDeclaration variable, TreeNode where) {
113 if (variable.flags & VariableDeclaration.FlagInScope == 0) { 145 if (variable.flags & VariableDeclaration.FlagInScope == 0) {
114 throw 'Variable $variable used out of scope in $context ' 146 problem(where, "Variable '$variable' used out of scope.");
115 '(${where.location})';
116 } 147 }
117 } 148 }
118 149
119 visitProgram(Program program) { 150 visitProgram(Program program) {
120 try { 151 try {
121 for (var library in program.libraries) { 152 for (var library in program.libraries) {
122 for (var class_ in library.classes) { 153 for (var class_ in library.classes) {
123 if (!classes.add(class_)) { 154 if (!classes.add(class_)) {
124 throw 'Class $class_ declared more than once.'; 155 problem(class_, "Class '$class_' declared more than once.");
125 } 156 }
126 } 157 }
127 library.members.forEach(declareMember); 158 library.members.forEach(declareMember);
128 for (var class_ in library.classes) { 159 for (var class_ in library.classes) {
129 class_.members.forEach(declareMember); 160 class_.members.forEach(declareMember);
130 } 161 }
131 } 162 }
132 visitChildren(program); 163 visitChildren(program);
133 } finally { 164 } finally {
134 for (var library in program.libraries) { 165 for (var library in program.libraries) {
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
201 232
202 visitFunctionNode(FunctionNode node) { 233 visitFunctionNode(FunctionNode node) {
203 declareTypeParameters(node.typeParameters); 234 declareTypeParameters(node.typeParameters);
204 visitWithLocalScope(node); 235 visitWithLocalScope(node);
205 undeclareTypeParameters(node.typeParameters); 236 undeclareTypeParameters(node.typeParameters);
206 } 237 }
207 238
208 visitFunctionType(FunctionType node) { 239 visitFunctionType(FunctionType node) {
209 for (int i = 1; i < node.namedParameters.length; ++i) { 240 for (int i = 1; i < node.namedParameters.length; ++i) {
210 if (node.namedParameters[i - 1].compareTo(node.namedParameters[i]) >= 0) { 241 if (node.namedParameters[i - 1].compareTo(node.namedParameters[i]) >= 0) {
211 throw 'Named parameters are not sorted on function type found in ' 242 problem(currentParent,
212 '$context'; 243 "Named parameters are not sorted on function type ($node).");
213 } 244 }
214 } 245 }
215 declareTypeParameters(node.typeParameters); 246 declareTypeParameters(node.typeParameters);
216 for (var typeParameter in node.typeParameters) { 247 for (var typeParameter in node.typeParameters) {
217 typeParameter.bound?.accept(this); 248 typeParameter.bound?.accept(this);
218 } 249 }
219 visitList(node.positionalParameters, this); 250 visitList(node.positionalParameters, this);
220 visitList(node.namedParameters, this); 251 visitList(node.namedParameters, this);
221 node.returnType.accept(this); 252 node.returnType.accept(this);
222 undeclareTypeParameters(node.typeParameters); 253 undeclareTypeParameters(node.typeParameters);
(...skipping 30 matching lines...) Expand all
253 284
254 visitVariableSet(VariableSet node) { 285 visitVariableSet(VariableSet node) {
255 checkVariableInScope(node.variable, node); 286 checkVariableInScope(node.variable, node);
256 visitChildren(node); 287 visitChildren(node);
257 } 288 }
258 289
259 @override 290 @override
260 visitStaticGet(StaticGet node) { 291 visitStaticGet(StaticGet node) {
261 visitChildren(node); 292 visitChildren(node);
262 if (node.target == null) { 293 if (node.target == null) {
263 throw 'StaticGet without target found in $context.'; 294 problem(node, "StaticGet without target.");
264 } 295 }
265 if (!node.target.hasGetter) { 296 if (!node.target.hasGetter) {
266 throw 'StaticGet to ${node.target} without getter found in $context'; 297 problem(node, "StaticGet of '${node.target}' without getter.");
267 } 298 }
268 if (node.target.isInstanceMember) { 299 if (node.target.isInstanceMember) {
269 throw 'StaticGet to ${node.target} that is not static found in $context'; 300 problem(node, "StaticGet of '${node.target}' that's an instance member.");
270 } 301 }
271 } 302 }
272 303
273 @override 304 @override
274 visitStaticSet(StaticSet node) { 305 visitStaticSet(StaticSet node) {
275 visitChildren(node); 306 visitChildren(node);
276 if (node.target == null) { 307 if (node.target == null) {
277 throw 'StaticSet without target found in $context.'; 308 problem(node, "StaticSet without target.");
278 } 309 }
279 if (!node.target.hasSetter) { 310 if (!node.target.hasSetter) {
280 throw 'StaticSet to ${node.target} without setter found in $context'; 311 problem(node, "StaticSet to '${node.target}' without setter.");
281 } 312 }
282 if (node.target.isInstanceMember) { 313 if (node.target.isInstanceMember) {
283 throw 'StaticSet to ${node.target} that is not static found in $context'; 314 problem(node, "StaticSet to '${node.target}' that's an instance member.");
284 } 315 }
285 } 316 }
286 317
287 @override 318 @override
288 visitStaticInvocation(StaticInvocation node) { 319 visitStaticInvocation(StaticInvocation node) {
289 visitChildren(node); 320 checkTargetedInvocation(node.target, node);
290 if (node.target == null) { 321 if (node.target.isInstanceMember) {
291 throw 'StaticInvocation without target found in $context.'; 322 problem(node,
323 "StaticInvocation of '${node.target}' that's an instance member.");
292 } 324 }
293 if (node.target.isInstanceMember) { 325 if (node.isConst &&
294 throw 'StaticInvocation to ${node.target} that is not static found in ' 326 (!node.target.isConst || !node.target.isExternal ||
295 '$context'; 327 node.target.kind != ProcedureKind.Factory)) {
296 } 328 problem(node, "Constant StaticInvocation of '${node.target}' that isn't"
297 if (!areArgumentsCompatible(node.arguments, node.target.function)) { 329 " a const external factory.");
298 throw 'StaticInvocation with incompatible arguments to '
299 '${node.target} found in $context';
300 }
301 if (node.arguments.types.length !=
302 node.target.function.typeParameters.length) {
303 throw 'Wrong number of type arguments provided in StaticInvocation '
304 'to ${node.target} found in $context';
305 } 330 }
306 } 331 }
307 332
333 void checkTargetedInvocation(Member target, InvocationExpression node) {
334 visitChildren(node);
335 if (target == null) {
336 problem(node, "${node.runtimeType} without target.");
337 }
338 if (target.function == null) {
339 problem(node, "${node.runtimeType} without function.");
340 }
341 if (!areArgumentsCompatible(node.arguments, target.function)) {
342 problem(node,
343 "${node.runtimeType} with incompatible arguments for '${target}'.");
344 }
345 int expectedTypeParameters = target is Constructor
346 ? target.enclosingClass.typeParameters.length
347 : target.function.typeParameters.length;
348 if (node.arguments.types.length != expectedTypeParameters) {
349 problem(node, "${node.runtimeType} with wrong number of type arguments"
350 " for '${target}'.");
351 }
352 }
353
308 @override 354 @override
309 visitDirectPropertyGet(DirectPropertyGet node) { 355 visitDirectPropertyGet(DirectPropertyGet node) {
310 visitChildren(node); 356 visitChildren(node);
311 if (node.target == null) { 357 if (node.target == null) {
312 throw 'DirectPropertyGet without target found in $context.'; 358 problem(node, "DirectPropertyGet without target.");
313 } 359 }
314 if (!node.target.hasGetter) { 360 if (!node.target.hasGetter) {
315 throw 'DirectPropertyGet to ${node.target} without getter found in ' 361 problem(node, "DirectPropertyGet of '${node.target}' without getter.");
316 '$context';
317 } 362 }
318 if (!node.target.isInstanceMember) { 363 if (!node.target.isInstanceMember) {
319 throw 'DirectPropertyGet to ${node.target} that is static found in ' 364 problem(node, "DirectPropertyGet of '${node.target}' that isn't an"
320 '$context'; 365 " instance member.");
321 } 366 }
322 } 367 }
323 368
324 @override 369 @override
325 visitDirectPropertySet(DirectPropertySet node) { 370 visitDirectPropertySet(DirectPropertySet node) {
326 visitChildren(node); 371 visitChildren(node);
327 if (node.target == null) { 372 if (node.target == null) {
328 throw 'DirectPropertySet without target found in $context.'; 373 problem(node, "DirectPropertySet without target.");
329 } 374 }
330 if (!node.target.hasSetter) { 375 if (!node.target.hasSetter) {
331 throw 'DirectPropertyGet to ${node.target} without setter found in ' 376 problem(node, "DirectPropertySet of '${node.target}' without setter.");
332 '$context';
333 } 377 }
334 if (!node.target.isInstanceMember) { 378 if (!node.target.isInstanceMember) {
335 throw 'DirectPropertySet to ${node.target} that is static found in ' 379 problem(node, "DirectPropertySet of '${node.target}' that is static.");
336 '$context';
337 } 380 }
338 } 381 }
339 382
340 @override 383 @override
341 visitDirectMethodInvocation(DirectMethodInvocation node) { 384 visitDirectMethodInvocation(DirectMethodInvocation node) {
342 visitChildren(node); 385 checkTargetedInvocation(node.target, node);
343 if (node.target == null) { 386 if (node.receiver == null) {
344 throw 'DirectMethodInvocation without target found in $context.'; 387 problem(node, "DirectMethodInvocation without receiver.");
345 }
346 if (!node.target.isInstanceMember) {
347 throw 'DirectMethodInvocation to ${node.target} that is static found in '
348 '$context';
349 }
350 if (!areArgumentsCompatible(node.arguments, node.target.function)) {
351 throw 'DirectMethodInvocation with incompatible arguments to '
352 '${node.target} found in $context';
353 }
354 if (node.arguments.types.length !=
355 node.target.function.typeParameters.length) {
356 throw 'Wrong number of type arguments provided in DirectMethodInvocation '
357 'to ${node.target} found in $context';
358 } 388 }
359 } 389 }
360 390
361 @override 391 @override
362 visitConstructorInvocation(ConstructorInvocation node) { 392 visitConstructorInvocation(ConstructorInvocation node) {
363 visitChildren(node); 393 checkTargetedInvocation(node.target, node);
364 if (node.target == null) { 394 if (node.target.enclosingClass.isAbstract) {
365 throw 'ConstructorInvocation without target found in $context.'; 395 problem(node, "ConstructorInvocation of abstract class.");
366 } 396 }
367 if (node.target.enclosingClass.isAbstract) { 397 if (node.isConst && !node.target.isConst) {
368 throw 'ConstructorInvocation to abstract class found in $context'; 398 problem(node, "Constant ConstructorInvocation fo '${node.target}' that"
369 } 399 " isn't const.");
370 if (!areArgumentsCompatible(node.arguments, node.target.function)) {
371 throw 'ConstructorInvocation with incompatible arguments to '
372 '${node.target} found in $context';
373 }
374 if (node.arguments.types.length !=
375 node.target.enclosingClass.typeParameters.length) {
376 throw 'Wrong number of type arguments provided in ConstructorInvocation '
377 'to ${node.target} found in $context';
378 } 400 }
379 } 401 }
380 402
381 bool areArgumentsCompatible(Arguments arguments, FunctionNode function) { 403 bool areArgumentsCompatible(Arguments arguments, FunctionNode function) {
382 if (arguments.positional.length < function.requiredParameterCount) { 404 if (arguments.positional.length < function.requiredParameterCount) {
383 return false; 405 return false;
384 } 406 }
385 if (arguments.positional.length > function.positionalParameters.length) { 407 if (arguments.positional.length > function.positionalParameters.length) {
386 return false; 408 return false;
387 } 409 }
388 namedLoop: 410 namedLoop:
389 for (int i = 0; i < arguments.named.length; ++i) { 411 for (int i = 0; i < arguments.named.length; ++i) {
390 var argument = arguments.named[i]; 412 var argument = arguments.named[i];
391 String name = argument.name; 413 String name = argument.name;
392 for (int j = 0; j < function.namedParameters.length; ++j) { 414 for (int j = 0; j < function.namedParameters.length; ++j) {
393 if (function.namedParameters[j].name == name) continue namedLoop; 415 if (function.namedParameters[j].name == name) continue namedLoop;
394 } 416 }
395 return false; 417 return false;
396 } 418 }
397 return true; 419 return true;
398 } 420 }
399 421
400 @override 422 @override
401 defaultMemberReference(Member node) { 423 defaultMemberReference(Member node) {
402 if (node.transformerFlags & TransformerFlag.seenByVerifier == 0) { 424 if (node.transformerFlags & TransformerFlag.seenByVerifier == 0) {
403 throw 'Dangling reference to $node found in $context.\n' 425 problem(node,
404 'Parent pointer is set to ${node.parent}'; 426 "Dangling reference to '$node', parent is: '${node.parent}'.");
405 } 427 }
406 } 428 }
407 429
408 @override 430 @override
409 visitClassReference(Class node) { 431 visitClassReference(Class node) {
410 if (!classes.contains(node)) { 432 if (!classes.contains(node)) {
411 throw 'Dangling reference to $node found in $context.\n' 433 problem(node,
412 'Parent pointer is set to ${node.parent}'; 434 "Dangling reference to '$node', parent is: '${node.parent}'.");
413 } 435 }
414 } 436 }
415 437
416 @override 438 @override
417 visitTypeParameterType(TypeParameterType node) { 439 visitTypeParameterType(TypeParameterType node) {
418 var parameter = node.parameter; 440 var parameter = node.parameter;
419 if (!typeParameters.contains(parameter)) { 441 if (!typeParameters.contains(parameter)) {
420 throw 'Type parameter $parameter referenced out of scope in $context.\n' 442 problem(currentParent, "Type parameter '$parameter' referenced out of"
421 'Parent pointer is set to ${parameter.parent}'; 443 " scope, parent is: '${parameter.parent}'.");
422 } 444 }
423 if (parameter.parent is Class && !classTypeParametersAreInScope) { 445 if (parameter.parent is Class && !classTypeParametersAreInScope) {
424 throw 'Type parameter $parameter referenced from static context ' 446 problem(currentParent, "Type parameter '$parameter' referenced from"
425 'in $context.\n' 447 " static context, parent is '${parameter.parent}'.");
426 'Parent pointer is set to ${parameter.parent}';
427 } 448 }
428 } 449 }
429 450
430 @override 451 @override
431 visitInterfaceType(InterfaceType node) { 452 visitInterfaceType(InterfaceType node) {
432 node.visitChildren(this); 453 node.visitChildren(this);
433 if (node.typeArguments.length != node.classNode.typeParameters.length) { 454 if (node.typeArguments.length != node.classNode.typeParameters.length) {
434 throw 'Type $node provides ${node.typeArguments.length} type arguments ' 455 problem(currentParent, "Type $node provides ${node.typeArguments.length}"
435 'but the class declares ${node.classNode.typeParameters.length} ' 456 " type arguments but the class declares"
436 'parameters. Found in $context.'; 457 " ${node.classNode.typeParameters.length} parameters.");
437 } 458 }
438 } 459 }
439 } 460 }
440 461
441 class CheckParentPointers extends Visitor { 462 class CheckParentPointers extends Visitor {
442 static void check(TreeNode node) { 463 static void check(TreeNode node) {
443 node.accept(new CheckParentPointers(node.parent)); 464 node.accept(new CheckParentPointers(node.parent));
444 } 465 }
445 466
446 TreeNode parent; 467 TreeNode parent;
447 468
448 CheckParentPointers([this.parent]); 469 CheckParentPointers([this.parent]);
449 470
450 defaultTreeNode(TreeNode node) { 471 defaultTreeNode(TreeNode node) {
451 if (node.parent != parent) { 472 if (node.parent != parent) {
452 throw 'Parent pointer on ${node.runtimeType} ' 473 throw new VerificationError(parent, node,
453 'is ${node.parent.runtimeType} ' 474 "Parent pointer on '${node.runtimeType}' "
454 'but should be ${parent.runtimeType}'; 475 "is '${node.parent.runtimeType}' "
476 "but should be '${parent.runtimeType}'.");
455 } 477 }
456 var oldParent = parent; 478 var oldParent = parent;
457 parent = node; 479 parent = node;
458 node.visitChildren(this); 480 node.visitChildren(this);
459 parent = oldParent; 481 parent = oldParent;
460 } 482 }
461 } 483 }
462 484
463 void checkInitializers(Constructor constructor) { 485 void checkInitializers(Constructor constructor) {
464 // TODO(ahe): I'll add more here in other CLs. 486 // TODO(ahe): I'll add more here in other CLs.
465 } 487 }
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698