/** * @license * Copyright 2012 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * Utility functions for handling variable and procedure names. * * @class */ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.Names'); import {Msg} from './msg.js'; // import * as Procedures from './procedures.js'; import type {VariableMap} from './variable_map.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; /** * Class for a database of entity names (variables, procedures, etc). * * @alias Blockly.Names */ export class Names { static DEVELOPER_VARIABLE_TYPE: NameType; private readonly variablePrefix_: string; /** A set of reserved words. */ private readonly reservedWords: Set; /** * A map from type (e.g. name, procedure) to maps from names to generated * names. */ private readonly db = new Map>(); /** A set of used names to avoid collisions. */ private readonly dbReverse = new Set(); /** * The variable map from the workspace, containing Blockly variable models. */ private variableMap_: VariableMap|null = null; /** * @param reservedWordsList A comma-separated string of words that are illegal * for use as names in a language (e.g. 'new,if,this,...'). * @param opt_variablePrefix Some languages need a '$' or a namespace before * all variable names (but not procedure names). */ constructor(reservedWordsList: string, opt_variablePrefix?: string) { /** The prefix to attach to variable names in generated code. */ this.variablePrefix_ = opt_variablePrefix || ''; this.reservedWords = new Set(reservedWordsList ? reservedWordsList.split(',') : []); } /** * Empty the database and start from scratch. The reserved words are kept. */ reset() { this.db.clear(); this.dbReverse.clear(); this.variableMap_ = null; } /** * Set the variable map that maps from variable name to variable object. * * @param map The map to track. */ setVariableMap(map: VariableMap) { this.variableMap_ = map; } /** * Get the name for a user-defined variable, based on its ID. * This should only be used for variables of NameType VARIABLE. * * @param id The ID to look up in the variable map. * @returns The name of the referenced variable, or null if there was no * variable map or the variable was not found in the map. */ private getNameForUserVariable_(id: string): string|null { if (!this.variableMap_) { console.warn( 'Deprecated call to Names.prototype.getName without ' + 'defining a variable map. To fix, add the following code in your ' + 'generator\'s init() function:\n' + 'Blockly.YourGeneratorName.nameDB_.setVariableMap(' + 'workspace.getVariableMap());'); return null; } const variable = this.variableMap_.getVariableById(id); if (variable) { return variable.name; } return null; } /** * Generate names for user variables, but only ones that are being used. * * @param workspace Workspace to generate variables from. */ populateVariables(workspace: Workspace) { const variables = Variables.allUsedVarModels(workspace); for (let i = 0; i < variables.length; i++) { this.getName(variables[i].getId(), NameType.VARIABLE); } } /** * Generate names for procedures. * * @param workspace Workspace to generate procedures from. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars populateProcedures(workspace: Workspace) { throw new Error( 'The implementation of populateProcedures should be ' + 'monkey-patched in by blockly.ts'); } /** * Convert a Blockly entity name to a legal exportable entity name. * * @param nameOrId The Blockly entity name (no constraints) or variable ID. * @param type The type of the name in Blockly ('VARIABLE', 'PROCEDURE', * 'DEVELOPER_VARIABLE', etc...). * @returns An entity name that is legal in the exported language. */ getName(nameOrId: string, type: NameType|string): string { let name = nameOrId; if (type === NameType.VARIABLE) { const varName = this.getNameForUserVariable_(nameOrId); if (varName) { // Successful ID lookup. name = varName; } } const normalizedName = name.toLowerCase(); const isVar = type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE; const prefix = isVar ? this.variablePrefix_ : ''; if (!this.db.has(type)) { this.db.set(type, new Map()); } const typeDb = this.db.get(type); if (typeDb!.has(normalizedName)) { return prefix + typeDb!.get(normalizedName); } const safeName = this.getDistinctName(name, type); typeDb!.set(normalizedName, safeName.substr(prefix.length)); return safeName; } /** * Return a list of all known user-created names of a specified name type. * * @param type The type of entity in Blockly ('VARIABLE', 'PROCEDURE', * 'DEVELOPER_VARIABLE', etc...). * @returns A list of Blockly entity names (no constraints). */ getUserNames(type: NameType|string): string[] { const userNames = this.db.get(type)?.keys(); return userNames ? Array.from(userNames) : []; } /** * Convert a Blockly entity name to a legal exportable entity name. * Ensure that this is a new name not overlapping any previously defined name. * Also check against list of reserved words for the current language and * ensure name doesn't collide. * * @param name The Blockly entity name (no constraints). * @param type The type of entity in Blockly ('VARIABLE', 'PROCEDURE', * 'DEVELOPER_VARIABLE', etc...). * @returns An entity name that is legal in the exported language. */ getDistinctName(name: string, type: NameType|string): string { let safeName = this.safeName_(name); let i = ''; while (this.dbReverse.has(safeName + i) || this.reservedWords.has(safeName + i)) { // Collision with existing name. Create a unique name. // AnyDuringMigration because: Type 'string | 2' is not assignable to // type 'string'. i = (i ? i + 1 : 2) as AnyDuringMigration; } safeName += i; this.dbReverse.add(safeName); const isVar = type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE; const prefix = isVar ? this.variablePrefix_ : ''; return prefix + safeName; } /** * Given a proposed entity name, generate a name that conforms to the * [_A-Za-z][_A-Za-z0-9]* format that most languages consider legal for * variable and function names. * * @param name Potentially illegal entity name. * @returns Safe entity name. */ private safeName_(name: string): string { if (!name) { name = Msg['UNNAMED_KEY'] || 'unnamed'; } else { // Unfortunately names in non-latin characters will look like // _E9_9F_B3_E4_B9_90 which is pretty meaningless. // https://github.com/google/blockly/issues/1654 name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_'); // Most languages don't allow names with leading numbers. if ('0123456789'.indexOf(name[0]) !== -1) { name = 'my_' + name; } } return name; } /** * Do the given two entity names refer to the same entity? * Blockly names are case-insensitive. * * @param name1 First name. * @param name2 Second name. * @returns True if names are the same. */ static equals(name1: string, name2: string): boolean { // name1.localeCompare(name2) is slower. return name1.toLowerCase() === name2.toLowerCase(); } } export namespace Names { /** * Enum for the type of a name. Different name types may have different rules * about collisions. * When JavaScript (or most other languages) is generated, variable 'foo' and * procedure 'foo' would collide. However, Blockly has no such problems since * variable get 'foo' and procedure call 'foo' are unambiguous. * Therefore, Blockly keeps a separate name type to disambiguate. * getName('foo', 'VARIABLE') = 'foo' * getName('foo', 'PROCEDURE') = 'foo2' * * @alias Blockly.Names.NameType */ export enum NameType { DEVELOPER_VARIABLE = 'DEVELOPER_VARIABLE', VARIABLE = 'VARIABLE', PROCEDURE = 'PROCEDURE', } } export type NameType = Names.NameType; export const NameType = Names.NameType; /** * Constant to separate developer variable names from user-defined variable * names when running generators. * A developer variable will be declared as a global in the generated code, but * will never be shown to the user in the workspace or stored in the variable * map. */ Names.DEVELOPER_VARIABLE_TYPE = NameType.DEVELOPER_VARIABLE;