blob: 6769bdda15af8d7fb18fcf417467170bc3b6061c [file] [log] [blame]
// Copyright (c) 2015, 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 discoveryapis_generator.namer;
import 'utils.dart';
/// Represents an identifier that can be given a name.
class Identifier {
String _name;
bool _sealed = false;
int _callCount = 0;
/// The preferred name for this [Identifier].
final String preferredName;
/// Used for naming prefix imports which will not get a name.
Identifier.noPrefix() : preferredName = null {
sealWithName(null);
}
/// Constructs a new [Identifier] with the given [preferredName]. The
/// identifier will be not sealed.
Identifier(this.preferredName);
bool get hasPrefix => preferredName != null;
/// The allocated name for this [Identifier]. This will be [:null:] until
/// [sealWithName] was called.
String get name => _name;
/// Seals this [Identifier] and gives it the name [name].
void sealWithName(String name) {
if (_sealed) {
throw StateError('This Identifier(preferredName: $preferredName) '
'has already been sealed.');
}
_name = name;
_sealed = true;
}
/// Return the reference name with a `.` appended (e.g., `core.`). Calling
/// this method will increment the call count; it is not idempotent.
String ref() {
_callCount++;
return name == null ? '' : '$name.';
}
bool get wasCalled => _callCount > 0;
/// Gets a string representation of this [Identifier]. This can only be called
/// after the identifier has been given a name.
@override
String toString() {
if (!_sealed) {
throw StateError('This Identifier(preferredName: $preferredName) '
'has not been sealed yet.');
}
return _name;
}
}
/// Allocate [Identifier]s for a lexical scope.
class Scope {
static final RegExp _startsWithDigit = RegExp('^[0-9]');
static final RegExp _nonAscii = RegExp('[^a-zA-z0-9]');
final Scope parentScope;
final List<Scope> childScopes = <Scope>[];
final List<Identifier> identifiers = <Identifier>[];
Scope({Scope parent}) : parentScope = parent;
/// Returns a valid identifier based on [preferredName] but different from all
/// other names previously returned by this method.
Identifier newIdentifier(String preferredName,
{bool removeUnderscores = true, bool global = false}) {
final identifier = Identifier(Scope.toValidIdentifier(preferredName,
removeUnderscores: removeUnderscores, global: global));
identifiers.add(identifier);
return identifier;
}
/// Creates a new child [Scope].
Scope newChildScope() {
final child = Scope(parent: this);
childScopes.add(child);
return child;
}
/// Converts [preferredName] to a valid identifier.
static String toValidIdentifier(String preferredName,
{bool removeUnderscores = true, bool global = false}) {
// Replace all abc_xyz with abcXyz.
if (removeUnderscores) {
preferredName =
Scope.capitalizeAtChar(preferredName, '_', keepEnding: true);
}
preferredName = preferredName.replaceAll('-', '_').replaceAll('.', '_');
preferredName = preferredName.replaceAll(_nonAscii, '_');
if (preferredName.startsWith(_startsWithDigit)) {
preferredName = 'D$preferredName';
} else if (preferredName.startsWith('_')) {
preferredName = 'P$preferredName';
}
if (keywords.contains(preferredName)) {
preferredName = '${preferredName}_';
}
if (global) {
preferredName = '\$$preferredName';
}
return preferredName;
}
static String toValidScopeName(String scope) {
const googleAuthPrefix = 'https://www.googleapis.com/auth/';
const httpsPrefix = 'https://';
// Defined by openid-connect-core 1.0
const openidScopes = {
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
'openid',
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
'profile',
'email',
'address',
'phone',
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
'offline_access',
};
String name;
if (scope.startsWith(googleAuthPrefix)) {
name = scope.substring(googleAuthPrefix.length);
} else if (scope.startsWith(httpsPrefix)) {
name = scope.substring(httpsPrefix.length);
} else if (openidScopes.contains(scope)) {
name = scope;
} else {
throw ArgumentError('Scope $scope is not a recognized format.');
}
name = Scope.capitalizeAtChar(name, '.');
name = Scope.capitalizeAtChar(name, '-');
name = Scope.capitalizeAtChar(name, '_');
name = Scope.capitalizeAtChar(name, '/');
return toValidIdentifier('${name}Scope');
}
static String capitalizeAtChar(
String name,
String char, {
bool keepEnding = false,
}) {
var index = -1;
while ((index = name.indexOf(char, 1)) > 0) {
if (index == (name.length - 1)) {
if (!keepEnding) {
// Drop [char] at the end of the string.
name = name.substring(0, name.length - 1);
} else {
break;
}
} else {
// Drop [char] and make the next character and uppercase.
final a = name.substring(0, index);
final b = name.substring(index + 1, index + 2);
final c = name.substring(index + 2);
name = '$a${b.toUpperCase()}$c';
}
}
return name;
}
/// Converts the first letter of [name] to an uppercase letter.
static String capitalize(String name) =>
'${name.substring(0, 1).toUpperCase()}${name.substring(1)}';
}
/// Names [Identifier]s and avoids name collisions by renaming.
///
/// For every named [Identifier], it's allocated name will be added to
/// [allocatedNames].
///
/// When allocating a new name, a name collides if either the name collides
/// with the [parentNamer] or if the name ia already in [allocatedNames].
///
/// When allocating a new name, the namer starts with the [Identifier]s
/// preferred name, and keeps appending _N where N is an integer until a name
/// does not collide.
class IdentifierNamer {
final IdentifierNamer parentNamer;
final Set<String> allocatedNames;
/// If [parentNamer] is given, this namer will only allocated names which are
/// - not taken by [parentNamer]
/// - not in [allocatedNames]
IdentifierNamer({this.parentNamer}) : allocatedNames = <String>{};
/// Reserves all given [allocatedNames] by default.
IdentifierNamer.fromNameSet(this.allocatedNames) : parentNamer = null;
/// Gives [Identifier] a unique name amongst all previously named identifiers
/// and amongst all identifiers of [parentNamer].
void nameIdentifier(Identifier identifier) {
final preferredName = identifier.preferredName;
var i = 0;
var currentName = preferredName;
while (_contains(currentName)) {
i++;
currentName = '${preferredName}_$i';
}
identifier.sealWithName(currentName);
allocatedNames.add(currentName);
}
bool _contains(String name) {
if (allocatedNames.contains(name)) return true;
if (parentNamer != null) {
if (parentNamer._contains(name)) return true;
}
return false;
}
}
/// Helper class for allocating unique names for generating an API library.
class ApiLibraryNamer {
String apiClassSuffix;
Scope _libraryScope;
/// NOTE: Only exposed for testing.
final Scope importScope = Scope();
ApiLibraryNamer({this.apiClassSuffix = 'Api'}) {
_libraryScope = importScope.newChildScope();
}
/// NOTE: Only exposed for testing.
Scope get libraryScope => _libraryScope;
String libraryName(String package, String api, String version) {
package = Scope.toValidIdentifier(package, removeUnderscores: false);
api = Scope.toValidIdentifier(api, removeUnderscores: false);
version = Scope.toValidIdentifier(version, removeUnderscores: false);
return '$package.$api.$version';
}
String clientLibraryName(String package, String api) {
package = Scope.toValidIdentifier(package, removeUnderscores: false);
api = Scope.toValidIdentifier(api, removeUnderscores: false);
return '$package.$api.client';
}
Identifier noPrefix() => Identifier.noPrefix();
Identifier import(String name) =>
importScope.newIdentifier(name, removeUnderscores: false);
Identifier apiClass(String name) =>
_libraryScope.newIdentifier('${Scope.capitalize(name)}$apiClassSuffix');
Identifier resourceClass(String name, {String parent}) {
name = Scope.capitalize(name);
if (parent != null && parent.isNotEmpty) {
// The parent of a resource is either the api class or another resource!
if (!parent.endsWith('Api')) {
throw ArgumentError('The parent has to end with Api');
}
final parentIsApiClass = !parent.endsWith('ResourceApi');
if (parentIsApiClass) {
// We never prefix resource names with the api class name.
parent = '';
} else {
parent = parent.substring(0, parent.length - 'ResourceApi'.length);
}
name = '$parent$name';
}
return _libraryScope.newIdentifier('${Scope.capitalize(name)}ResourceApi');
}
String schemaClassName(String name, {String parent}) {
if (parent != null) {
name = '$parent${Scope.capitalize(name)}';
}
return Scope.capitalize(name);
}
Identifier schemaClass(String name) =>
_libraryScope.newIdentifier(Scope.capitalize(name));
Scope newClassScope() => _libraryScope.newChildScope();
void nameAllIdentifiers() {
//
// This method implements the following algorithm:
// a) name all [Identifier]s in the library scope:
// => api class, schema classes, resource classes
// b) name all [Identifier]s in the class scopes
// => fields and methods
// c) name all [Identifier]s in the method parameter lists
// => positional parameters, optional parameters
// d) name all [Identifier]s in the import scope
//
// This is implicitly done by
// - naming all [Indentifier]s (**) in scope X
// - naming all child scopes (**) of X
// (**) (which are already ordered)
//
// The import scope is root of a scope tree which contains all [Identifier]s
//
// Collisions are handled in the a),b),c) phases by renaming if a name
// was already taken by either the current scope or a parent scope.
//
// Collisions are handled in the d) phase by renaming if a name was already
// taken by any of the other scopes
// (which names get collected in [allAllocatedNames])
// => This makes sure we rather rename a import than a method parameter,
// but still try to name imports with preferred names if possible.
// [e.g. if a method parameter is named 'core' we will rename the
// import to: import 'dart:core' as core_1;
final allAllocatedNames = <String>{};
void nameScope(Scope scope, parentResolver) {
final resolver = IdentifierNamer(parentNamer: parentResolver);
scope.identifiers.forEach(resolver.nameIdentifier);
// Order does not matter because child scopes are independent of each
// other.
for (var childScope in scope.childScopes) {
nameScope(childScope, resolver);
}
allAllocatedNames.addAll(resolver.allocatedNames);
}
// Name library scope identifiers and down. the passed [IdentifierNamer] is
// an empty root namer.
nameScope(_libraryScope, IdentifierNamer());
// Name all import identifiers. In case we have clashes with any of the
// other names already assigned, we'll rename the prefixed imports.
final resolver = IdentifierNamer.fromNameSet(allAllocatedNames);
importScope.identifiers.forEach(resolver.nameIdentifier);
}
}