| // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library analyzer.src.dart.sdk.patch; |
| |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/error/listener.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/src/dart/scanner/reader.dart'; |
| import 'package:analyzer/src/dart/scanner/scanner.dart'; |
| import 'package:analyzer/src/generated/parser.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:meta/meta.dart'; |
| |
| /** |
| * [SdkPatcher] applies patches to SDK [CompilationUnit]. |
| */ |
| class SdkPatcher { |
| bool _allowNewPublicNames; |
| String _baseDesc; |
| String _patchDesc; |
| CompilationUnit _patchUnit; |
| |
| /** |
| * Patch the given [unit] of a SDK [source] with the patches defined in |
| * [allPatchPaths]. Throw [ArgumentError] if a patch |
| * file cannot be read, or the contents violates rules for patch files. |
| */ |
| void patch( |
| ResourceProvider resourceProvider, |
| bool strongMode, |
| Map<String, List<String>> allPatchPaths, |
| AnalysisErrorListener errorListener, |
| Source source, |
| CompilationUnit unit) { |
| // Process URI. |
| String libraryUriStr; |
| bool isLibraryDefiningUnit; |
| { |
| Uri uri = source.uri; |
| if (uri.scheme != 'dart') { |
| throw new ArgumentError( |
| 'The URI of the unit to patch must have the "dart" scheme: $uri'); |
| } |
| List<String> uriSegments = uri.pathSegments; |
| String libraryName = uriSegments.first; |
| libraryUriStr = 'dart:$libraryName'; |
| isLibraryDefiningUnit = uriSegments.length == 1; |
| _allowNewPublicNames = libraryName == '_internal'; |
| } |
| // Prepare the patch files to apply. |
| List<String> patchPaths = allPatchPaths[libraryUriStr] ?? const <String>[]; |
| |
| for (String path in patchPaths) { |
| File patchFile = resourceProvider.getFile(path); |
| if (!patchFile.exists) { |
| throw new ArgumentError( |
| 'The patch file ${patchFile.path} for $source does not exist.'); |
| } |
| Source patchSource = patchFile.createSource(); |
| CompilationUnit patchUnit = parse(patchSource, strongMode, errorListener); |
| |
| // Prepare for reporting errors. |
| _baseDesc = source.toString(); |
| _patchDesc = patchFile.path; |
| _patchUnit = patchUnit; |
| |
| if (isLibraryDefiningUnit) { |
| _patchDirectives(source, unit, patchSource, patchUnit); |
| } |
| _patchTopLevelDeclarations(unit, patchUnit, isLibraryDefiningUnit); |
| } |
| } |
| |
| void _failExternalKeyword(String name, int offset) { |
| throw new ArgumentError( |
| 'The keyword "external" was expected for "$name" in $_baseDesc @ $offset.'); |
| } |
| |
| void _failIfPublicName(AstNode node, String name) { |
| if (_allowNewPublicNames) { |
| return; |
| } |
| if (!Identifier.isPrivateName(name)) { |
| _failInPatch('contains a public declaration "$name"', node.offset); |
| } |
| } |
| |
| void _failInPatch(String message, int offset) { |
| String loc = _getLocationDesc3(_patchUnit, offset); |
| throw new ArgumentError( |
| 'The patch file $_patchDesc for $_baseDesc $message at $loc.'); |
| } |
| |
| String _getLocationDesc3(CompilationUnit unit, int offset) { |
| LineInfo_Location location = unit.lineInfo.getLocation(offset); |
| return 'the line ${location.lineNumber}'; |
| } |
| |
| void _matchParameterLists(FormalParameterList baseParameters, |
| FormalParameterList patchParameters, String context()) { |
| if (baseParameters == null && patchParameters == null) return; |
| if (baseParameters == null || patchParameters == null) { |
| throw new ArgumentError("${context()}, parameter lists don't match"); |
| } |
| if (baseParameters.parameters.length != patchParameters.parameters.length) { |
| throw new ArgumentError( |
| '${context()}, parameter lists have different lengths'); |
| } |
| for (var i = 0; i < baseParameters.parameters.length; i++) { |
| _matchParameters(baseParameters.parameters[i], |
| patchParameters.parameters[i], () => '${context()}, parameter $i'); |
| } |
| } |
| |
| void _matchParameters(FormalParameter baseParameter, |
| FormalParameter patchParameter, String whichParameter()) { |
| if (baseParameter.identifier.name != patchParameter.identifier.name) { |
| throw new ArgumentError('${whichParameter()} has different name'); |
| } |
| NormalFormalParameter baseParameterWithoutDefault = |
| _withoutDefault(baseParameter); |
| NormalFormalParameter patchParameterWithoutDefault = |
| _withoutDefault(patchParameter); |
| if (baseParameterWithoutDefault is SimpleFormalParameter && |
| patchParameterWithoutDefault is SimpleFormalParameter) { |
| _matchTypes(baseParameterWithoutDefault.type, |
| patchParameterWithoutDefault.type, () => '${whichParameter()} type'); |
| } else if (baseParameterWithoutDefault is FunctionTypedFormalParameter && |
| patchParameterWithoutDefault is FunctionTypedFormalParameter) { |
| _matchTypes( |
| baseParameterWithoutDefault.returnType, |
| patchParameterWithoutDefault.returnType, |
| () => '${whichParameter()} return type'); |
| _matchParameterLists( |
| baseParameterWithoutDefault.parameters, |
| patchParameterWithoutDefault.parameters, |
| () => '${whichParameter()} parameters'); |
| } else if (baseParameterWithoutDefault is FieldFormalParameter && |
| patchParameter is FieldFormalParameter) { |
| throw new ArgumentError( |
| '${whichParameter()} cannot be patched (field formal parameters are not supported)'); |
| } else { |
| throw new ArgumentError( |
| '${whichParameter()} mismatch (different parameter kinds)'); |
| } |
| } |
| |
| void _matchTypes(TypeName baseType, TypeName patchType, String whichType()) { |
| error() => new ArgumentError("${whichType()} doesn't match"); |
| if (baseType == null && patchType == null) return; |
| if (baseType == null || patchType == null) throw error(); |
| // Match up the types token by token; this is more restrictive than strictly |
| // necessary, but it's easy and sufficient for patching purposes. |
| Token baseToken = baseType.beginToken; |
| Token patchToken = patchType.beginToken; |
| while (true) { |
| if (baseToken.lexeme != patchToken.lexeme) throw error(); |
| if (identical(baseToken, baseType.endToken) && |
| identical(patchToken, patchType.endToken)) { |
| break; |
| } |
| if (identical(baseToken, baseType.endToken) || |
| identical(patchToken, patchType.endToken)) { |
| throw error(); |
| } |
| baseToken = baseToken.next; |
| patchToken = patchToken.next; |
| } |
| } |
| |
| void _patchClassMembers( |
| ClassDeclaration baseClass, ClassDeclaration patchClass) { |
| String className = baseClass.name.name; |
| List<ClassMember> membersToAppend = []; |
| for (ClassMember patchMember in patchClass.members) { |
| if (patchMember is FieldDeclaration) { |
| if (_hasPatchAnnotation(patchMember.metadata)) { |
| _failInPatch('attempts to patch a field', patchMember.offset); |
| } |
| List<VariableDeclaration> fields = patchMember.fields.variables; |
| if (fields.length != 1) { |
| _failInPatch('contains a field declaration with more than one field', |
| patchMember.offset); |
| } |
| String name = fields[0].name.name; |
| if (!_allowNewPublicNames && |
| !Identifier.isPrivateName(className) && |
| !Identifier.isPrivateName(name)) { |
| _failInPatch('contains a public field', patchMember.offset); |
| } |
| membersToAppend.add(patchMember); |
| } else if (patchMember is MethodDeclaration) { |
| String name = patchMember.name.name; |
| if (_hasPatchAnnotation(patchMember.metadata)) { |
| for (ClassMember baseMember in baseClass.members) { |
| if (baseMember is MethodDeclaration && |
| baseMember.name.name == name) { |
| // Remove the "external" keyword. |
| Token externalKeyword = baseMember.externalKeyword; |
| if (externalKeyword != null) { |
| baseMember.externalKeyword = null; |
| _removeToken(externalKeyword); |
| } else { |
| _failExternalKeyword(name, baseMember.offset); |
| } |
| _matchParameterLists( |
| baseMember.parameters, |
| patchMember.parameters, |
| () => 'While patching $className.$name'); |
| _matchTypes(baseMember.returnType, patchMember.returnType, |
| () => 'While patching $className.$name, return type'); |
| // Replace the body. |
| FunctionBody oldBody = baseMember.body; |
| FunctionBody newBody = patchMember.body; |
| _replaceNodeTokens(oldBody, newBody); |
| baseMember.body = newBody; |
| } |
| } |
| } else { |
| _failIfPublicName(patchMember, name); |
| membersToAppend.add(patchMember); |
| } |
| } else if (patchMember is ConstructorDeclaration) { |
| String name = patchMember.name?.name; |
| if (_hasPatchAnnotation(patchMember.metadata)) { |
| for (ClassMember baseMember in baseClass.members) { |
| if (baseMember is ConstructorDeclaration && |
| baseMember.name?.name == name) { |
| // Remove the "external" keyword. |
| Token externalKeyword = baseMember.externalKeyword; |
| if (externalKeyword != null) { |
| baseMember.externalKeyword = null; |
| _removeToken(externalKeyword); |
| } else { |
| _failExternalKeyword(name, baseMember.offset); |
| } |
| // Factory vs. generative. |
| if (baseMember.factoryKeyword == null && |
| patchMember.factoryKeyword != null) { |
| _failInPatch( |
| 'attempts to replace generative constructor with a factory one', |
| patchMember.offset); |
| } else if (baseMember.factoryKeyword != null && |
| patchMember.factoryKeyword == null) { |
| _failInPatch( |
| 'attempts to replace factory constructor with a generative one', |
| patchMember.offset); |
| } |
| // The base constructor should not have initializers. |
| if (baseMember.initializers.isNotEmpty) { |
| throw new ArgumentError( |
| 'Cannot patch external constructors with initializers ' |
| 'in $_baseDesc.'); |
| } |
| _matchParameterLists( |
| baseMember.parameters, patchMember.parameters, () { |
| String nameSuffix = name == null ? '' : '.$name'; |
| return 'While patching $className$nameSuffix'; |
| }); |
| // Prepare nodes. |
| FunctionBody baseBody = baseMember.body; |
| FunctionBody patchBody = patchMember.body; |
| NodeList<ConstructorInitializer> baseInitializers = |
| baseMember.initializers; |
| NodeList<ConstructorInitializer> patchInitializers = |
| patchMember.initializers; |
| // Replace initializers and link tokens. |
| if (patchInitializers.isNotEmpty) { |
| baseMember.parameters.endToken |
| .setNext(patchInitializers.beginToken.previous); |
| baseInitializers.addAll(patchInitializers); |
| patchBody.endToken.setNext(baseBody.endToken.next); |
| } else { |
| _replaceNodeTokens(baseBody, patchBody); |
| } |
| // Replace the body. |
| baseMember.body = patchBody; |
| } |
| } |
| } else { |
| if (name == null) { |
| if (!_allowNewPublicNames && !Identifier.isPrivateName(className)) { |
| _failInPatch( |
| 'contains an unnamed public constructor', patchMember.offset); |
| } |
| } else { |
| _failIfPublicName(patchMember, name); |
| } |
| membersToAppend.add(patchMember); |
| } |
| } else { |
| String className = patchClass.name.name; |
| _failInPatch('contains an unsupported class member in $className', |
| patchMember.offset); |
| } |
| } |
| // Append new class members. |
| _appendToNodeList( |
| baseClass.members, membersToAppend, baseClass.leftBracket); |
| } |
| |
| void _patchDirectives(Source baseSource, CompilationUnit baseUnit, |
| Source patchSource, CompilationUnit patchUnit) { |
| for (Directive patchDirective in patchUnit.directives) { |
| if (patchDirective is ImportDirective) { |
| baseUnit.directives.add(patchDirective); |
| } else { |
| _failInPatch('contains an unsupported "$patchDirective" directive', |
| patchDirective.offset); |
| } |
| } |
| } |
| |
| void _patchTopLevelDeclarations(CompilationUnit baseUnit, |
| CompilationUnit patchUnit, bool appendNewTopLevelDeclarations) { |
| List<CompilationUnitMember> declarationsToAppend = []; |
| for (CompilationUnitMember patchDeclaration in patchUnit.declarations) { |
| if (patchDeclaration is FunctionDeclaration) { |
| String name = patchDeclaration.name.name; |
| if (_hasPatchAnnotation(patchDeclaration.metadata)) { |
| for (CompilationUnitMember baseDeclaration in baseUnit.declarations) { |
| if (patchDeclaration is FunctionDeclaration && |
| baseDeclaration is FunctionDeclaration && |
| baseDeclaration.name.name == name) { |
| // Remove the "external" keyword. |
| Token externalKeyword = baseDeclaration.externalKeyword; |
| if (externalKeyword != null) { |
| baseDeclaration.externalKeyword = null; |
| _removeToken(externalKeyword); |
| } else { |
| _failExternalKeyword(name, baseDeclaration.offset); |
| } |
| _matchParameterLists( |
| baseDeclaration.functionExpression.parameters, |
| patchDeclaration.functionExpression.parameters, |
| () => 'While patching $name'); |
| _matchTypes( |
| baseDeclaration.returnType, |
| patchDeclaration.returnType, |
| () => 'While patching $name, return type'); |
| // Replace the body. |
| FunctionExpression oldExpr = baseDeclaration.functionExpression; |
| FunctionBody newBody = patchDeclaration.functionExpression.body; |
| _replaceNodeTokens(oldExpr.body, newBody); |
| oldExpr.body = newBody; |
| } |
| } |
| } else if (appendNewTopLevelDeclarations) { |
| _failIfPublicName(patchDeclaration, name); |
| declarationsToAppend.add(patchDeclaration); |
| } |
| } else if (patchDeclaration is FunctionTypeAlias) { |
| if (patchDeclaration.metadata.isNotEmpty) { |
| _failInPatch('contains a function type alias with an annotation', |
| patchDeclaration.offset); |
| } |
| _failIfPublicName(patchDeclaration, patchDeclaration.name.name); |
| declarationsToAppend.add(patchDeclaration); |
| } else if (patchDeclaration is ClassDeclaration) { |
| if (_hasPatchAnnotation(patchDeclaration.metadata)) { |
| String name = patchDeclaration.name.name; |
| for (CompilationUnitMember baseDeclaration in baseUnit.declarations) { |
| if (baseDeclaration is ClassDeclaration && |
| baseDeclaration.name.name == name) { |
| _patchClassMembers(baseDeclaration, patchDeclaration); |
| } |
| } |
| } else { |
| _failIfPublicName(patchDeclaration, patchDeclaration.name.name); |
| declarationsToAppend.add(patchDeclaration); |
| } |
| } else if (patchDeclaration is TopLevelVariableDeclaration && |
| !_hasPatchAnnotation(patchDeclaration.metadata)) { |
| for (VariableDeclaration variable |
| in patchDeclaration.variables.variables) { |
| _failIfPublicName(patchDeclaration, variable.name.name); |
| } |
| declarationsToAppend.add(patchDeclaration); |
| } else { |
| _failInPatch('contains an unsupported top-level declaration', |
| patchDeclaration.offset); |
| } |
| } |
| // Append new top-level declarations. |
| if (appendNewTopLevelDeclarations) { |
| _appendToNodeList(baseUnit.declarations, declarationsToAppend, |
| baseUnit.endToken.previous); |
| } |
| } |
| |
| NormalFormalParameter _withoutDefault(FormalParameter parameter) { |
| if (parameter is NormalFormalParameter) { |
| return parameter; |
| } else if (parameter is DefaultFormalParameter) { |
| return parameter.parameter; |
| } else { |
| // Should not happen. |
| assert(false); |
| return null; |
| } |
| } |
| |
| /** |
| * Parse the given [source] into AST. |
| */ |
| @visibleForTesting |
| static CompilationUnit parse( |
| Source source, bool strong, AnalysisErrorListener errorListener) { |
| String code = source.contents.data; |
| |
| CharSequenceReader reader = new CharSequenceReader(code); |
| Scanner scanner = new Scanner(source, reader, errorListener); |
| scanner.scanGenericMethodComments = strong; |
| Token token = scanner.tokenize(); |
| LineInfo lineInfo = new LineInfo(scanner.lineStarts); |
| |
| Parser parser = new Parser(source, errorListener); |
| parser.parseGenericMethodComments = strong; |
| CompilationUnit unit = parser.parseCompilationUnit(token); |
| unit.lineInfo = lineInfo; |
| return unit; |
| } |
| |
| /** |
| * Append [newNodes] to the given [nodes] and attach new tokens to the end |
| * token of the last [nodes] items, or, if it is empty, to [defaultPrevToken]. |
| */ |
| static void _appendToNodeList( |
| NodeList<AstNode> nodes, List<AstNode> newNodes, Token defaultPrevToken) { |
| Token prevToken = nodes.endToken ?? defaultPrevToken; |
| for (AstNode newNode in newNodes) { |
| newNode.endToken.setNext(prevToken.next); |
| prevToken.setNext(newNode.beginToken); |
| nodes.add(newNode); |
| prevToken = newNode.endToken; |
| } |
| } |
| |
| /** |
| * Return `true` if [metadata] has the `@patch` annotation. |
| */ |
| static bool _hasPatchAnnotation(List<Annotation> metadata) { |
| return metadata.any((annotation) { |
| Identifier name = annotation.name; |
| return annotation.constructorName == null && |
| name is SimpleIdentifier && |
| name.name == 'patch'; |
| }); |
| } |
| |
| /** |
| * Remove the [token] from the stream. |
| */ |
| static void _removeToken(Token token) { |
| token.previous.setNext(token.next); |
| } |
| |
| /** |
| * Replace tokens of the [oldNode] with tokens of the [newNode]. |
| */ |
| static void _replaceNodeTokens(AstNode oldNode, AstNode newNode) { |
| oldNode.beginToken.previous.setNext(newNode.beginToken); |
| newNode.endToken.setNext(oldNode.endToken.next); |
| } |
| } |