Skip to content

Commit c297f46

Browse files
authored
Improved field validators (#338)
* Extracted code from mrc_class_method_def functions findLegalMethodName and isMethodNameUsed to make function makeLegalName in validator.ts. Updated mrc_class_method_def, mrc_component, mrc_event, and mrc_mechanism blocks to use makeLegalName. * Changed makeLegalName optionally return a valid python identifier. * Use makeLegalName to make names unique when mechanisms, components, and events are dragged into the holder. Use makeLegalName to validate parameter names. * Update comment about case insensitive
1 parent 810a7cb commit c297f46

File tree

8 files changed

+145
-100
lines changed

8 files changed

+145
-100
lines changed

src/blocks/mrc_class_method_def.ts

Lines changed: 17 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import * as toolboxItems from '../toolbox/items';
3535
import { getClassData } from './utils/python';
3636
import { FunctionData } from './utils/python_json_types';
3737
import { findConnectedBlocksOfType } from './utils/find_connected_blocks';
38+
import { makeLegalName } from './utils/validator';
3839
import { NONCOPYABLE_BLOCK } from './noncopyable_block';
3940
import { BLOCK_NAME as MRC_GET_PARAMETER_BLOCK_NAME } from './mrc_get_parameter';
4041
import * as paramContainer from './mrc_param_container'
@@ -293,10 +294,23 @@ const CLASS_METHOD_DEF = {
293294
// When the user changes the method name on the block, clear the mrcPythonMethodName field.
294295
this.mrcPythonMethodName = '';
295296

296-
// Strip leading and trailing whitespace.
297-
name = name.trim();
297+
if (this.isInFlyout) {
298+
// Flyouts can have multiple methods with identical names.
299+
return name;
300+
}
301+
302+
const otherNames: string[] = [];
303+
this.workspace.getBlocksByType(BLOCK_NAME)
304+
.filter(block => block.id !== this.id)
305+
.forEach((block) => {
306+
otherNames.push(block.getFieldValue(FIELD_METHOD_NAME));
307+
const classMethodDefBlock = block as ClassMethodDefBlock;
308+
if (classMethodDefBlock.mrcPythonMethodName) {
309+
otherNames.push(classMethodDefBlock.mrcPythonMethodName);
310+
}
311+
});
298312

299-
const legalName = findLegalMethodName(name, this);
313+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ true);
300314
const oldName = nameField.getValue();
301315
if (oldName && oldName !== name && oldName !== legalName) {
302316
// Rename any callers.
@@ -374,61 +388,6 @@ const CLASS_METHOD_DEF = {
374388
},
375389
};
376390

377-
/**
378-
* Ensure two identically-named methods don't exist.
379-
* Take the proposed method name, and return a legal name i.e. one that
380-
* is not empty and doesn't collide with other methods.
381-
*
382-
* @param name Proposed method name.
383-
* @param block Block to disambiguate.
384-
* @returns Non-colliding name.
385-
*/
386-
function findLegalMethodName(name: string, block: ClassMethodDefBlock): string {
387-
if (block.isInFlyout) {
388-
// Flyouts can have multiple methods called 'my_method'.
389-
return name;
390-
}
391-
name = name || 'unnamed';
392-
while (isMethodNameUsed(name, block.workspace, block)) {
393-
// Collision with another method.
394-
const r = name.match(/^(.*?)(\d+)$/);
395-
if (!r) {
396-
name += '2';
397-
} else {
398-
name = r[1] + (parseInt(r[2]) + 1);
399-
}
400-
}
401-
return name;
402-
}
403-
404-
/**
405-
* Return if the given name is already a method name.
406-
*
407-
* @param name The questionable name.
408-
* @param workspace The workspace to scan for collisions.
409-
* @param opt_exclude Optional block to exclude from comparisons (one doesn't
410-
* want to collide with oneself).
411-
* @returns True if the name is used, otherwise return false.
412-
*/
413-
function isMethodNameUsed(
414-
name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean {
415-
const nameLowerCase = name.toLowerCase();
416-
for (const block of workspace.getBlocksByType(BLOCK_NAME)) {
417-
if (block === opt_exclude) {
418-
continue;
419-
}
420-
if (nameLowerCase === block.getFieldValue(FIELD_METHOD_NAME).toLowerCase()) {
421-
return true;
422-
}
423-
const classMethodDefBlock = block as ClassMethodDefBlock;
424-
if (classMethodDefBlock.mrcPythonMethodName &&
425-
nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) {
426-
return true;
427-
}
428-
}
429-
return false;
430-
}
431-
432391
export const setup = function () {
433392
Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF;
434393
};

src/blocks/mrc_component.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Editor } from '../editor/editor';
2828
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2929
import { getModuleTypeForWorkspace } from './utils/workspaces';
3030
import { getAllowedTypesForSetCheck, getClassData, getSubclassNames } from './utils/python';
31+
import { makeLegalName } from './utils/validator';
3132
import * as toolboxItems from '../toolbox/items';
3233
import * as storageModule from '../storage/module';
3334
import * as storageModuleContent from '../storage/module_content';
@@ -164,10 +165,19 @@ const COMPONENT = {
164165
}
165166
},
166167
mrcNameFieldValidator(this: ComponentBlock, nameField: Blockly.FieldTextInput, name: string): string {
167-
// Strip leading and trailing whitespace.
168-
name = name.trim();
168+
if (this.isInFlyout) {
169+
// Flyouts can have multiple methods with identical names.
170+
return name;
171+
}
172+
173+
const otherNames: string[] = [];
174+
this.workspace.getBlocksByType(BLOCK_NAME)
175+
.filter(block => block.id !== this.id)
176+
.forEach((block) => {
177+
otherNames.push(block.getFieldValue(FIELD_NAME));
178+
});
169179

170-
const legalName = name;
180+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ true);
171181
const oldName = nameField.getValue();
172182
if (oldName && oldName !== name && oldName !== legalName) {
173183
// Rename any callers.

src/blocks/mrc_event.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as Blockly from 'blockly';
2323

2424
import { MRC_STYLE_EVENTS } from '../themes/styles'
2525
import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
26+
import { makeLegalName } from './utils/validator';
2627
import { Parameter } from './mrc_class_method_def';
2728
import { Editor } from '../editor/editor';
2829
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
@@ -187,10 +188,19 @@ const EVENT = {
187188
});
188189
},
189190
mrcNameFieldValidator(this: EventBlock, nameField: Blockly.FieldTextInput, name: string): string {
190-
// Strip leading and trailing whitespace.
191-
name = name.trim();
191+
if (this.isInFlyout) {
192+
// Flyouts can have multiple methods with identical names.
193+
return name;
194+
}
195+
196+
const otherNames: string[] = [];
197+
this.workspace.getBlocksByType(BLOCK_NAME)
198+
.filter(block => block.id !== this.id)
199+
.forEach((block) => {
200+
otherNames.push(block.getFieldValue(FIELD_EVENT_NAME));
201+
});
192202

193-
const legalName = name;
203+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ false);
194204
const oldName = nameField.getValue();
195205
if (oldName && oldName !== name && oldName !== legalName) {
196206
// Rename any callers.

src/blocks/mrc_mechanism.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { createFieldNonEditableText } from '../fields/FieldNonEditableText';
2727
import { Editor } from '../editor/editor';
2828
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2929
import { getAllowedTypesForSetCheck } from './utils/python';
30+
import { makeLegalName } from './utils/validator';
3031
import * as toolboxItems from '../toolbox/items';
3132
import * as storageModule from '../storage/module';
3233
import * as storageModuleContent from '../storage/module_content';
@@ -167,7 +168,19 @@ const MECHANISM = {
167168
// Strip leading and trailing whitespace.
168169
name = name.trim();
169170

170-
const legalName = name;
171+
if (this.isInFlyout) {
172+
// Flyouts can have multiple methods with identical names.
173+
return name;
174+
}
175+
176+
const otherNames: string[] = [];
177+
this.workspace.getBlocksByType(BLOCK_NAME)
178+
.filter(block => block.id !== this.id)
179+
.forEach((block) => {
180+
otherNames.push(block.getFieldValue(FIELD_NAME));
181+
});
182+
183+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ true);
171184
const oldName = nameField.getValue();
172185
if (oldName && oldName !== name && oldName !== legalName) {
173186
// Rename any callers.

src/blocks/mrc_mechanism_component_holder.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import * as Blockly from 'blockly';
2323

2424
import { MRC_STYLE_MECHANISMS } from '../themes/styles';
25-
import { getLegalName } from './utils/python';
25+
import { makeLegalName } from './utils/validator';
2626
import { Editor } from '../editor/editor';
2727
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2828
import * as storageModule from '../storage/module';
@@ -220,14 +220,16 @@ const MECHANISM_COMPONENT_HOLDER = {
220220
*/
221221
setNameOfChildBlock(this: MechanismComponentHolderBlock, child: Blockly.Block): void {
222222
const otherNames: string[] = []
223-
const descendants = this.getDescendants(true);
224-
descendants
223+
this.getDescendants(true)
225224
.filter(descendant => descendant.id !== child.id)
226225
.forEach(descendant => {
227226
otherNames.push(descendant.getFieldValue('NAME'));
228227
});
229228
const currentName = child.getFieldValue('NAME');
230-
const legalName = getLegalName(currentName, otherNames);
229+
// mechanism and component names must be valid python identifiers.
230+
// event names do not need to be valid python identifiers.
231+
const mustBeValidPythonIdentifier = (child.type === MRC_MECHANISM_NAME || child.type === MRC_COMPONENT_NAME);
232+
const legalName = makeLegalName(currentName, otherNames, mustBeValidPythonIdentifier);
231233
if (legalName !== currentName) {
232234
child.setFieldValue(legalName, 'NAME');
233235
}

src/blocks/mrc_param_container.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222
import * as Blockly from 'blockly';
2323
import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles';
24-
import { getLegalName } from './utils/python';
24+
import { makeLegalName } from './utils/validator';
2525

2626
export const PARAM_CONTAINER_BLOCK_NAME = 'mrc_param_container';
2727
const PARAM_ITEM_BLOCK_NAME = 'mrc_param_item';
@@ -85,13 +85,16 @@ const PARAM_ITEM = {
8585
const rootBlock: Blockly.Block | null = this.getRootBlock();
8686
if (rootBlock) {
8787
const otherNames: string[] = []
88-
rootBlock!.getDescendants(true)?.forEach(itemBlock => {
89-
if (itemBlock != this) {
90-
otherNames.push(itemBlock.getFieldValue(FIELD_NAME));
91-
}
92-
});
88+
rootBlock.getDescendants(true)
89+
.filter(descendant => descendant.type === PARAM_ITEM_BLOCK_NAME && descendant !== this)
90+
.forEach(itemBlock => {
91+
if (itemBlock != this) {
92+
otherNames.push(itemBlock.getFieldValue(FIELD_NAME));
93+
}
94+
});
9395
const currentName = this.getFieldValue(FIELD_NAME);
94-
this.setFieldValue(getLegalName(currentName, otherNames), FIELD_NAME);
96+
const legalName = makeLegalName(currentName, otherNames, /* mustBeValidPythonIdentifier */ true);
97+
this.setFieldValue(legalName, FIELD_NAME);
9598
updateMutatorFlyout(this.workspace);
9699
}
97100
},

src/blocks/utils/python.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -281,30 +281,6 @@ export function getOutputCheck(type: string): string {
281281
return type;
282282
}
283283

284-
// Function to return a legal name based off of proposed names and making sure it doesn't conflict
285-
// This is a legal name for python methods and variables.
286-
export function getLegalName(proposedName: string, existingNames: string[]){
287-
let newName = proposedName.trim().replace(' ', '_');
288-
289-
// TODO: Allow the user to put numbers in the name.
290-
291-
if (!/^[A-Za-z_]/.test(newName)){
292-
newName = "_" + newName;
293-
}
294-
295-
while (existingNames.includes(newName)){
296-
const match = /(.*?)(\d+)$/.exec(newName)
297-
298-
if (match) {
299-
let lastNumber = +match[2]
300-
newName = match[1] + (lastNumber + 1)
301-
} else {
302-
newName += "2"
303-
}
304-
}
305-
return newName;
306-
}
307-
308284
export function isExistingPythonModule(moduleName: string): boolean {
309285
for (const pythonData of allPythonData) {
310286
// Process modules.

src/blocks/utils/validator.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @author [email protected] (Liz Looney)
20+
*/
21+
22+
/**
23+
* Take the proposed name, and return a legal name.
24+
* A legal name is:
25+
* 1. not empty.
26+
* 2. doesn't collide with other names (case insensitive).
27+
* 3. Optional: is a valid python identifier.
28+
*/
29+
export function makeLegalName(proposedName: string, otherNames: string[], mustBeValidPythonIdentifier: boolean): string {
30+
const otherNamesLowerCase = otherNames.map(n => n.toLowerCase());
31+
32+
// Strip leading and trailing whitespace.
33+
let name = proposedName.trim();
34+
35+
// Make the name non-empty.
36+
name = name || 'unnamed';
37+
38+
if (mustBeValidPythonIdentifier) {
39+
if (!name.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
40+
let identifier = '';
41+
const firstChar = name[0];
42+
if (/[a-zA-Z_]/.test(firstChar)) {
43+
identifier += firstChar;
44+
} else if (/[a-zA-Z0-9_]/.test(firstChar)) {
45+
// If the first character would be valid as the second charactor, insert an underscore.
46+
identifier += '_' + firstChar;
47+
} else {
48+
identifier += '_';
49+
}
50+
for (let i = 1; i < name.length; i++) {
51+
const char = name[i];
52+
if (/[a-zA-Z0-9_]/.test(char)) {
53+
identifier += char;
54+
} else {
55+
identifier += '_';
56+
}
57+
}
58+
name = identifier;
59+
}
60+
}
61+
62+
while (otherNamesLowerCase.includes(name.toLowerCase())) {
63+
// Collision with another name.
64+
const r = name.match(/^(.*?)(\d+)$/);
65+
if (!r) {
66+
name += '2';
67+
} else {
68+
name = r[1] + (parseInt(r[2]) + 1);
69+
}
70+
}
71+
return name;
72+
}

0 commit comments

Comments
 (0)