-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Allow duplicate registration of values #7924
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,11 +38,13 @@ const typeMap: { | |
export const TEST_ONLY = {typeMap}; | ||
|
||
/** | ||
* A map of maps. With the keys being the type and caseless name of the class we | ||
* are registring, and the value being the most recent cased name for that | ||
* registration. | ||
* A map of maps. Record the case-insensitive version of each name. | ||
* e.g. {'field': {'field_angle': 'fIeLd_AnGle'}} | ||
* This enables an error to be thrown if a name of conflicting case is used. | ||
*/ | ||
const nameMap: {[key: string]: {[key: string]: string}} = Object.create(null); | ||
const noCaseNameMap: { | ||
[key: string]: {[key: string]: string}; | ||
} = Object.create(null); | ||
|
||
/** | ||
* The string used to register the default class for a type of plugin. | ||
|
@@ -129,51 +131,33 @@ export function register<T>( | |
| AnyDuringMigration, | ||
opt_allowOverrides?: boolean, | ||
): void { | ||
if ( | ||
(!(type instanceof Type) && typeof type !== 'string') || | ||
`${type}`.trim() === '' | ||
) { | ||
throw Error( | ||
'Invalid type "' + | ||
type + | ||
'". The type must be a' + | ||
' non-empty string or a Blockly.registry.Type.', | ||
); | ||
} | ||
type = `${type}`.toLowerCase(); | ||
|
||
if (typeof name !== 'string' || name.trim() === '') { | ||
throw Error( | ||
'Invalid name "' + name + '". The name must be a' + ' non-empty string.', | ||
); | ||
} | ||
const caselessName = name.toLowerCase(); | ||
type = normalizeType(type); | ||
name = normalizeName(name); | ||
if (!registryItem) { | ||
throw Error('Can not register a null value'); | ||
} | ||
let typeRegistry = typeMap[type]; | ||
let nameRegistry = nameMap[type]; | ||
// If the type registry has not been created, create it. | ||
if (!typeRegistry) { | ||
typeRegistry = typeMap[type] = Object.create(null); | ||
nameRegistry = nameMap[type] = Object.create(null); | ||
noCaseNameMap[type] = Object.create(null); | ||
} | ||
|
||
// Validate that the given class has all the required properties. | ||
validate(type, registryItem); | ||
validateItem(type, registryItem); | ||
validateCase(type, name); | ||
|
||
// Don't throw an error if opt_allowOverrides is true. | ||
if (!opt_allowOverrides && typeRegistry[caselessName]) { | ||
throw Error( | ||
'Name "' + | ||
caselessName + | ||
'" with type "' + | ||
type + | ||
'" already registered.', | ||
); | ||
// Don't throw an error if the registryItem is not changing. | ||
if ( | ||
!opt_allowOverrides && | ||
typeRegistry[name] && | ||
typeRegistry[name] !== registryItem | ||
) { | ||
throw Error(`Name "${name}" with type "${type}" already registered`); | ||
} | ||
typeRegistry[caselessName] = registryItem; | ||
nameRegistry[caselessName] = name; | ||
typeRegistry[name] = registryItem; | ||
noCaseNameMap[type][name.toLowerCase()] = name; | ||
} | ||
|
||
/** | ||
|
@@ -184,16 +168,32 @@ export function register<T>( | |
* @param registryItem A class or object that we are checking for the required | ||
* properties. | ||
*/ | ||
function validate(type: string, registryItem: Function | AnyDuringMigration) { | ||
function validateItem( | ||
type: string, | ||
registryItem: Function | AnyDuringMigration, | ||
) { | ||
switch (type) { | ||
case String(Type.FIELD): | ||
if (typeof registryItem.fromJson !== 'function') { | ||
throw Error('Type "' + type + '" must have a fromJson function'); | ||
throw Error(`Type "${type}" must have a fromJson function`); | ||
} | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* Checks that the name hasn't been registered using a different case. | ||
* | ||
* @param type The type of the plugin. (e.g. Field, Renderer) | ||
* @param name The plugin's name. (Ex. field_angle, geras) | ||
*/ | ||
function validateCase(type: string, name: string) { | ||
const caseName = noCaseNameMap[type]?.[name.toLowerCase()]; | ||
if (caseName && caseName !== name) { | ||
throw Error(`Inconsistent case with name "${name}" vs "${caseName}"`); | ||
} | ||
} | ||
|
||
/** | ||
* Unregisters the registry item with the given type and name. | ||
* | ||
|
@@ -202,22 +202,16 @@ function validate(type: string, registryItem: Function | AnyDuringMigration) { | |
* @param name The plugin's name. (Ex. field_angle, geras) | ||
*/ | ||
export function unregister<T>(type: string | Type<T>, name: string) { | ||
type = `${type}`.toLowerCase(); | ||
name = name.toLowerCase(); | ||
type = normalizeType(type); | ||
name = normalizeName(name); | ||
validateCase(type, name); | ||
const typeRegistry = typeMap[type]; | ||
if (!typeRegistry || !typeRegistry[name]) { | ||
console.warn( | ||
'Unable to unregister [' + | ||
name + | ||
'][' + | ||
type + | ||
'] from the ' + | ||
'registry.', | ||
); | ||
console.warn(`Unable to unregister [${name}][${type}] from the registry`); | ||
return; | ||
} | ||
delete typeMap[type][name]; | ||
delete nameMap[type][name]; | ||
delete typeRegistry[name]; | ||
delete noCaseNameMap[type][name.toLowerCase()]; | ||
} | ||
|
||
/** | ||
|
@@ -237,15 +231,14 @@ function getItem<T>( | |
name: string, | ||
opt_throwIfMissing?: boolean, | ||
): (new (...p1: AnyDuringMigration[]) => T) | null | AnyDuringMigration { | ||
type = `${type}`.toLowerCase(); | ||
name = name.toLowerCase(); | ||
type = normalizeType(type); | ||
name = normalizeName(name); | ||
validateCase(type, name); | ||
const typeRegistry = typeMap[type]; | ||
if (!typeRegistry || !typeRegistry[name]) { | ||
const msg = 'Unable to find [' + name + '][' + type + '] in the registry.'; | ||
const msg = `Unable to find [${name}][${type}] in the registry.`; | ||
if (opt_throwIfMissing) { | ||
throw new Error( | ||
msg + ' You must require or register a ' + type + ' plugin.', | ||
); | ||
throw Error(`${msg} You must require or register a ${type} plugin`); | ||
} else { | ||
console.warn(msg); | ||
} | ||
|
@@ -265,15 +258,41 @@ function getItem<T>( | |
* otherwise. | ||
*/ | ||
export function hasItem<T>(type: string | Type<T>, name: string): boolean { | ||
type = `${type}`.toLowerCase(); | ||
name = name.toLowerCase(); | ||
type = normalizeType(type); | ||
name = normalizeName(name); | ||
validateCase(type, name); | ||
const typeRegistry = typeMap[type]; | ||
if (!typeRegistry) { | ||
return false; | ||
} | ||
return !!typeRegistry[name]; | ||
} | ||
|
||
/** | ||
* Normalize a string to be used as a type. Trim whitespaces and lowercase. | ||
* | ||
* @param type The type. (Ex. 'FiELd ') | ||
* @returns A normalized string. (Ex. 'field') | ||
*/ | ||
function normalizeType(type: any): string { | ||
type = `${type}`.trim().toLowerCase(); | ||
if (!type) throw Error('Empty type'); | ||
return type; | ||
} | ||
|
||
/** | ||
* Normalize a string to be used as a key. Trim whitespaces. | ||
* | ||
* @param name The key's name. (Ex. 'field_angle ') | ||
* @returns A normalized string. (Ex. 'field_angle') | ||
*/ | ||
function normalizeName(name: string): string { | ||
if (typeof name !== 'string') throw Error('Invalid name'); | ||
name = name.trim(); | ||
if (!name) throw Error('Empty name'); | ||
return name; | ||
} | ||
|
||
/** | ||
* Gets the class for the given name and type. | ||
* | ||
|
@@ -316,18 +335,20 @@ export function getObject<T>( | |
* Returns a map of items registered with the given type. | ||
* | ||
* @param type The type of the plugin. (e.g. Category) | ||
* @param opt_cased Whether or not to return a map with cased keys (rather than | ||
* caseless keys). False by default. | ||
* @param opt_cased Deprecated. | ||
* @param opt_throwIfMissing Whether or not to throw an error if we are unable | ||
* to find the object. False by default. | ||
* @returns A map of objects with the given type, or null if none exists. | ||
*/ | ||
export function getAllItems<T>( | ||
type: string | Type<T>, | ||
opt_cased?: boolean, | ||
opt_cased?: any, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately after looking into it I'm no longer sure this is safe to remove, at least not without doing some work to make the serializer case-insensitive. If someone registered a serializer with an uppercase name, then their existing serialized workspace data would have uppercase characters in it. After this change, if we tried to deserialize that data, the names wouldn't match and the data wouldn't load correctly. @BeksOmega can you confirm that? Would this be safe if we changed serialization to also lowercase the state keys when loading? I guess if someone registered both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I believe if someone has registered a serializer with capitals (which is would be reasonable if they were using Given that, I have a small opinion that this is not worth changing. [Edit: missed this]
That should be safe except about what you mentioned with |
||
opt_throwIfMissing?: boolean, | ||
): {[key: string]: T | null | (new (...p1: AnyDuringMigration[]) => T)} | null { | ||
type = `${type}`.toLowerCase(); | ||
if (typeof opt_cased !== 'undefined') { | ||
console.warn('Blockly.registry.getAllItems deprecated "cased" argument'); | ||
} | ||
type = normalizeType(type); | ||
const typeRegistry = typeMap[type]; | ||
if (!typeRegistry) { | ||
const msg = `Unable to find [${type}] in the registry.`; | ||
|
@@ -338,15 +359,7 @@ export function getAllItems<T>( | |
} | ||
return null; | ||
} | ||
if (!opt_cased) { | ||
return typeRegistry; | ||
} | ||
const nameRegistry = nameMap[type]; | ||
const casedRegistry = Object.create(null); | ||
for (const key of Object.keys(typeRegistry)) { | ||
casedRegistry[nameRegistry[key]] = typeRegistry[key]; | ||
} | ||
return casedRegistry; | ||
return typeRegistry; | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is now a breaking change in the other direction, we previously allowed you to ignore case (except for serializers, where the name you pass in is actually in the serialized output) so throwing an error now will break anyone who was relying on that.
There might still be some cleanup that's possible here but I don't think we should change the behavior in either direction, in other words, we live with the somewhat weird state that most registries are case-insensitive except for the serializer. And the cleanup doesn't need to block the allow duplicate registration of values since that's required for v11 so there's some urgency there, unlike the cleanup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current PR will break two cases:
I'd argue that both of these behaviours are bugs.
It would be one thing if we could just leave this mess alone, but we need to build on top of it for V11. This problem is going to contiune to compound, and we'll have a registry that behaves in random ways and is impossible to document. I think if we don't straighten the registry out now, it's just going to get worse and worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's fair to call those behaviors bugs because we explicitly built support for them (i.e. the parameter you can pass to allow for case insensitivity). In hindsight, we may have made a different decision around this if we went back in time. But now, if we want to change it, we need to do a proper cleanup which would mean weighing the value of this breaking change against the burden of communicating it to users and forcing them to do work to change their code if they're affected by it. Part of that proper cleanup would be doing a real breaking change, communicating it effectively, and not having it be mixed with the feature of allowing duplicate registration.
I hear you that we're building on top of mess, but the actual change we need for v11 doesn't have anything to do with case sensitivity and is really a very small change buried in the much larger case sensitivity refactoring part of this PR, so I don't think that's a compelling enough reason to mix the two issues.
If you want to file an issue for the case insensitivity cleanup, we can triage it separately during our normal bug triage process. I would prefer you do just the duplicate registration of values in this PR, and wait to see how prioritization works out for the rest of the cleanup vs everything else we already have planned for the v11 release