blob: 88a122eecf2423a57cc680fb4b15873553790c4e [file] [log] [blame]
// 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/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/parser.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:meta/meta.dart';
import 'package:path/src/context.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
* the [sdk] for the given [platform]. Throw [ArgumentError] if a patch
* file cannot be read, or the contents violates rules for patch files.
*/
void patch(
FolderBasedDartSdk sdk,
int platform,
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;
{
SdkLibrary sdkLibrary = sdk.getSdkLibrary(libraryUriStr);
if (sdkLibrary == null) {
throw new ArgumentError(
'The library $libraryUriStr is not defined in the SDK.');
}
patchPaths = sdkLibrary.getPatches(platform);
}
bool strongMode = sdk.analysisOptions.strongMode;
Context pathContext = sdk.resourceProvider.pathContext;
for (String path in patchPaths) {
String pathInLib = pathContext.joinAll(path.split('/'));
File patchFile = sdk.libraryDirectory.getChildAssumingFile(pathInLib);
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 _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);
}
// 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.');
}
// 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);
}
// 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 {
_failInPatch('contains an unsupported top-level declaration',
patchDeclaration.offset);
}
}
// Append new top-level declarations.
if (appendNewTopLevelDeclarations) {
_appendToNodeList(baseUnit.declarations, declarationsToAppend,
baseUnit.endToken.previous);
}
}
/**
* 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);
}
}