Optimize performance (#1355)

This commit is contained in:
Timofey Kachalov
2025-12-16 23:02:58 +04:00
committed by GitHub
parent 6866396e9a
commit 12aff3cfd4
13 changed files with 214 additions and 229 deletions
@@ -77,6 +77,10 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme
* @returns {boolean}
*/
private static canTransformBlockStatementNode(blockStatementNode: ESTree.BlockStatement): boolean {
if (blockStatementNode.body.length <= 4) {
return false;
}
let canTransform: boolean = true;
estraverse.traverse(blockStatementNode, {
@@ -91,10 +95,6 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme
}
});
if (blockStatementNode.body.length <= 4) {
canTransform = false;
}
return canTransform;
}
@@ -138,8 +138,11 @@ export class BlockStatementControlFlowTransformer extends AbstractNodeTransforme
const blockStatementBody: ESTree.Statement[] = blockStatementNode.body;
const originalKeys: number[] = this.arrayUtils.createWithRange(blockStatementBody.length);
const shuffledKeys: number[] = this.arrayUtils.shuffle(originalKeys);
const originalKeysIndexesInShuffledArray: number[] = originalKeys.map((key: number) =>
shuffledKeys.indexOf(key)
const shuffledKeyToIndex: Map<number, number> = new Map(
shuffledKeys.map((key: number, index: number) => [key, index])
);
const originalKeysIndexesInShuffledArray: number[] = originalKeys.map(
(key: number) => shuffledKeyToIndex.get(key)!
);
const blockStatementControlFlowFlatteningCustomNode: ICustomNode<
TInitialData<BlockStatementControlFlowFlatteningNode>
@@ -68,10 +68,10 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
* @returns {string}
*/
public generateStorageKey(controlFlowStorage: IControlFlowStorage): string {
const key: string = this.randomGenerator.getRandomString(5);
let key: string = this.randomGenerator.getRandomString(5);
if (controlFlowStorage.has(key)) {
return this.generateStorageKey(controlFlowStorage);
while (controlFlowStorage.has(key)) {
key = this.randomGenerator.getRandomString(5);
}
return key;
@@ -91,9 +91,21 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
usingExistingIdentifierChance: number
): string {
const controlFlowStorageId: string = controlFlowStorage.getStorageId();
const storageKeysById: Map<string | number, string[]> =
this.replacerDataByControlFlowStorageId.get(controlFlowStorageId) ?? new Map<string, string[]>();
const storageKeysForCurrentId: string[] = storageKeysById.get(replacerId) ?? [];
let storageKeysById: Map<string | number, string[]> | undefined =
this.replacerDataByControlFlowStorageId.get(controlFlowStorageId);
if (!storageKeysById) {
storageKeysById = new Map<string | number, string[]>();
this.replacerDataByControlFlowStorageId.set(controlFlowStorageId, storageKeysById);
}
let storageKeysForCurrentId: string[] | undefined = storageKeysById.get(replacerId);
if (!storageKeysForCurrentId) {
storageKeysForCurrentId = [];
storageKeysById.set(replacerId, storageKeysForCurrentId);
}
const shouldPickFromStorageKeysById =
this.randomGenerator.getMathRandom() < usingExistingIdentifierChance && storageKeysForCurrentId.length;
@@ -105,8 +117,6 @@ export abstract class AbstractControlFlowReplacer implements IControlFlowReplace
const storageKey: string = this.generateStorageKey(controlFlowStorage);
storageKeysForCurrentId.push(storageKey);
storageKeysById.set(replacerId, storageKeysForCurrentId);
this.replacerDataByControlFlowStorageId.set(controlFlowStorageId, storageKeysById);
controlFlowStorage.set(storageKey, customNode);
return storageKey;
@@ -31,9 +31,9 @@ import { NodeGuards } from '../../node/NodeGuards';
@injectable()
export class ClassFieldTransformer extends AbstractNodeTransformer {
/**
* @type {string[]}
* @type {string}
*/
private static readonly ignoredNames: string[] = ['constructor'];
private static readonly ignoredName: string = 'constructor';
/**
* @param {IRandomGenerator} randomGenerator
@@ -98,7 +98,7 @@ export class ClassFieldTransformer extends AbstractNodeTransformer {
classFieldNode: ESTree.MethodDefinition | ESTree.PropertyDefinition,
keyNode: ESTree.Identifier
): ESTree.MethodDefinition | ESTree.PropertyDefinition {
if (!ClassFieldTransformer.ignoredNames.includes(keyNode.name) && !classFieldNode.computed) {
if (keyNode.name !== ClassFieldTransformer.ignoredName && !classFieldNode.computed) {
classFieldNode.computed = true;
classFieldNode.key = NodeFactory.literalNode(keyNode.name);
}
@@ -117,7 +117,7 @@ export class ClassFieldTransformer extends AbstractNodeTransformer {
): ESTree.MethodDefinition | ESTree.PropertyDefinition {
if (
typeof keyNode.value === 'string' &&
!ClassFieldTransformer.ignoredNames.includes(keyNode.value) &&
keyNode.value !== ClassFieldTransformer.ignoredName &&
!classFieldNode.computed
) {
classFieldNode.computed = true;
@@ -53,6 +53,61 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer {
this.objectExpressionExtractorFactory = objectExpressionExtractorFactory;
}
/**
* Combined prohibition check result
*/
private static checkProhibitedPatterns(
objectExpressionNode: ESTree.ObjectExpression,
objectExpressionHostNode: ESTree.Node
): { hasReferencedIdentifier: boolean; hasCallExpression: boolean } {
const identifierNamesSet: Set<string> = new Set();
let hasReferencedIdentifier: boolean = false;
let hasCallExpression: boolean = false;
let isInsideObjectExpression: boolean = false;
estraverse.traverse(objectExpressionHostNode, {
// eslint-disable-next-line complexity
enter: (node: ESTree.Node): void | estraverse.VisitorOption => {
if (node === objectExpressionNode) {
isInsideObjectExpression = true;
}
if (isInsideObjectExpression && !hasCallExpression) {
if (NodeGuards.isCallExpressionNode(node) || NodeGuards.isNewExpressionNode(node)) {
hasCallExpression = true;
}
}
if (NodeGuards.isIdentifierNode(node) || NodeGuards.isThisExpressionNode(node)) {
const identifierName: string = NodeGuards.isIdentifierNode(node)
? node.name
: ObjectExpressionKeysTransformer.thisIdentifierName;
if (!isInsideObjectExpression) {
identifierNamesSet.add(identifierName);
} else if (identifierNamesSet.has(identifierName)) {
hasReferencedIdentifier = true;
}
}
if (hasReferencedIdentifier && hasCallExpression) {
return estraverse.VisitorOption.Break;
}
},
leave: (node: ESTree.Node): void | estraverse.VisitorOption => {
if (node === objectExpressionNode) {
isInsideObjectExpression = false;
if (hasReferencedIdentifier || hasCallExpression) {
return estraverse.VisitorOption.Break;
}
}
}
});
return { hasReferencedIdentifier, hasCallExpression };
}
/**
* @param {ObjectExpression} objectExpressionNode
* @param {Node} objectExpressionParentNode
@@ -64,86 +119,25 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer {
objectExpressionParentNode: ESTree.Node,
objectExpressionHostStatement: ESTree.Statement
): boolean {
return (
ObjectExpressionKeysTransformer.isReferencedIdentifierName(
objectExpressionNode,
objectExpressionHostStatement
) ||
if (
ObjectExpressionKeysTransformer.isProhibitedArrowFunctionExpression(
objectExpressionNode,
objectExpressionParentNode
) ||
ObjectExpressionKeysTransformer.isObjectExpressionWithCallExpression(objectExpressionNode) ||
ObjectExpressionKeysTransformer.isProhibitedSequenceExpression(
objectExpressionNode,
objectExpressionHostStatement
)
);
}
/**
* @param {Identifier | ThisExpression} node
* @returns {string}
*/
private static getReferencedIdentifierName(node: ESTree.Identifier | ESTree.ThisExpression): string {
if (NodeGuards.isIdentifierNode(node)) {
return node.name;
} else {
return ObjectExpressionKeysTransformer.thisIdentifierName;
) {
return true;
}
}
/**
* @param {ObjectExpression} objectExpressionNode
* @param {Node} objectExpressionHostNode
* @returns {boolean}
*/
private static isReferencedIdentifierName(
objectExpressionNode: ESTree.ObjectExpression,
objectExpressionHostNode: ESTree.Node
): boolean {
const identifierNamesSet: Set<string> = new Set();
const { hasReferencedIdentifier, hasCallExpression } = ObjectExpressionKeysTransformer.checkProhibitedPatterns(
objectExpressionNode,
objectExpressionHostStatement
);
let isReferencedIdentifierName: boolean = false;
let isCurrentNode: boolean = false;
// should mark node as prohibited if identifier of node is referenced somewhere inside other nodes
estraverse.traverse(objectExpressionHostNode, {
enter: (node: ESTree.Node): void | estraverse.VisitorOption => {
if (node === objectExpressionNode) {
isCurrentNode = true;
}
if (!NodeGuards.isIdentifierNode(node) && !NodeGuards.isThisExpressionNode(node)) {
return;
}
if (!isCurrentNode) {
identifierNamesSet.add(ObjectExpressionKeysTransformer.getReferencedIdentifierName(node));
return;
}
const hasReferencedIdentifierName: boolean = identifierNamesSet.has(
ObjectExpressionKeysTransformer.getReferencedIdentifierName(node)
);
if (hasReferencedIdentifierName) {
isReferencedIdentifierName = true;
return estraverse.VisitorOption.Break;
}
},
leave: (node: ESTree.Node): void | estraverse.VisitorOption => {
if (node === objectExpressionNode) {
isCurrentNode = false;
return estraverse.VisitorOption.Break;
}
}
});
return isReferencedIdentifierName;
return hasReferencedIdentifier || hasCallExpression;
}
/**
@@ -161,31 +155,6 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer {
);
}
/**
* @param {ObjectExpression} objectExpressionNode
* @returns {boolean}
*/
private static isObjectExpressionWithCallExpression(objectExpressionNode: ESTree.ObjectExpression): boolean {
let isCallExpressionLikeNodeFound: boolean = false;
estraverse.traverse(objectExpressionNode, {
enter: (node: ESTree.Node): void | estraverse.VisitorOption => {
const isCallExpressionLikeNode =
NodeGuards.isCallExpressionNode(node) || NodeGuards.isNewExpressionNode(node);
if (!isCallExpressionLikeNode) {
return;
}
isCallExpressionLikeNodeFound = true;
return estraverse.VisitorOption.Break;
}
});
return isCallExpressionLikeNodeFound;
}
/**
* @param {ObjectExpression} objectExpressionNode
* @param {Node} objectExpressionHostNode
@@ -263,32 +232,28 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer {
return objectExpressionNode;
}
return this.applyObjectExpressionKeysExtractorsRecursive(
ObjectExpressionKeysTransformer.objectExpressionExtractorNames,
objectExpressionNode,
hostStatement
);
return this.applyObjectExpressionKeysExtractorsRecursive(objectExpressionNode, hostStatement, 0);
}
/**
* @param {ObjectExpressionExtractor[]} objectExpressionExtractorNames
* @param {ObjectExpression} objectExpressionNode
* @param {Statement} hostStatement
* @param {number} extractorIndex
* @returns {Node}
*/
private applyObjectExpressionKeysExtractorsRecursive(
objectExpressionExtractorNames: ObjectExpressionExtractor[],
objectExpressionNode: ESTree.ObjectExpression,
hostStatement: ESTree.Statement
hostStatement: ESTree.Statement,
extractorIndex: number
): ESTree.Node {
const newObjectExpressionExtractorNames: ObjectExpressionExtractor[] = [...objectExpressionExtractorNames];
const objectExpressionExtractor: ObjectExpressionExtractor | undefined =
newObjectExpressionExtractorNames.shift();
const objectExpressionExtractorNames = ObjectExpressionKeysTransformer.objectExpressionExtractorNames;
if (!objectExpressionExtractor) {
if (extractorIndex >= objectExpressionExtractorNames.length) {
return objectExpressionNode;
}
const objectExpressionExtractor: ObjectExpressionExtractor = objectExpressionExtractorNames[extractorIndex];
const {
nodeToReplace,
objectExpressionHostStatement: newObjectExpressionHostStatement,
@@ -299,9 +264,9 @@ export class ObjectExpressionKeysTransformer extends AbstractNodeTransformer {
);
this.applyObjectExpressionKeysExtractorsRecursive(
newObjectExpressionExtractorNames,
newObjectExpressionNode,
newObjectExpressionHostStatement
newObjectExpressionHostStatement,
extractorIndex + 1
);
return nodeToReplace;
@@ -152,26 +152,25 @@ export class SplitStringTransformer extends AbstractNodeTransformer {
* @returns {BinaryExpression}
*/
private transformStringChunksToBinaryExpressionNode(chunks: string[]): ESTree.BinaryExpression {
const firstChunk: string | undefined = chunks.shift();
const secondChunk: string | undefined = chunks.shift();
const chunksLength: number = chunks.length;
if (!firstChunk || !secondChunk) {
if (chunksLength < 2) {
throw new Error('First and second chunks values should not be empty');
}
const initialBinaryExpressionNode: ESTree.BinaryExpression = NodeFactory.binaryExpressionNode(
'+',
NodeFactory.literalNode(firstChunk),
NodeFactory.literalNode(secondChunk)
NodeFactory.literalNode(chunks[0]),
NodeFactory.literalNode(chunks[1])
);
return chunks.reduce<ESTree.BinaryExpression>(
(binaryExpressionNode: ESTree.BinaryExpression, chunk: string) => {
const chunkLiteralNode: ESTree.Literal = NodeFactory.literalNode(chunk);
let result: ESTree.BinaryExpression = initialBinaryExpressionNode;
return NodeFactory.binaryExpressionNode('+', binaryExpressionNode, chunkLiteralNode);
},
initialBinaryExpressionNode
);
// Start from index 2 since we already used 0 and 1
for (let i: number = 2; i < chunksLength; i++) {
result = NodeFactory.binaryExpressionNode('+', result, NodeFactory.literalNode(chunks[i]));
}
return result;
}
}
@@ -83,53 +83,60 @@ export class TemplateLiteralTransformer extends AbstractNodeTransformer {
): ESTree.Expression {
const templateLiteralExpressions: ESTree.Expression[] = templateLiteralNode.expressions;
let nodes: ESTree.Expression[] = [];
const nodes: ESTree.Expression[] = [];
const quasis: ESTree.TemplateElement[] = templateLiteralNode.quasis;
const quasisLength: number = quasis.length;
for (let i: number = 0; i < quasisLength; i++) {
const templateElement: ESTree.TemplateElement = quasis[i];
templateLiteralNode.quasis.forEach((templateElement: ESTree.TemplateElement) => {
if (templateElement.value.cooked === undefined || templateElement.value.cooked === null) {
return;
continue;
}
nodes.push(NodeFactory.literalNode(templateElement.value.cooked));
const expression: ESTree.Expression | undefined = templateLiteralExpressions.shift();
const expression: ESTree.Expression | undefined = templateLiteralExpressions[i];
if (!expression) {
return;
continue;
}
nodes.push(expression);
});
}
nodes = nodes.filter((node: ESTree.Literal | ESTree.Expression) => {
const filteredNodes: ESTree.Expression[] = nodes.filter((node: ESTree.Literal | ESTree.Expression) => {
return !(NodeGuards.isLiteralNode(node) && node.value === '');
});
// since `+` is left-to-right associative
// ensure the first node is a string if first/second isn't
if (
!TemplateLiteralTransformer.isLiteralNodeWithStringValue(nodes[0]) &&
!TemplateLiteralTransformer.isLiteralNodeWithStringValue(nodes[1])
!TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[0]) &&
!TemplateLiteralTransformer.isLiteralNodeWithStringValue(filteredNodes[1])
) {
nodes.unshift(NodeFactory.literalNode(''));
filteredNodes.unshift(NodeFactory.literalNode(''));
}
let transformedNode: ESTree.Node;
if (nodes.length > 1) {
if (filteredNodes.length > 1) {
let root: ESTree.BinaryExpression = NodeFactory.binaryExpressionNode(
'+',
<ESTree.Literal>nodes.shift(),
<ESTree.Expression>nodes.shift()
<ESTree.Literal>filteredNodes[0],
filteredNodes[1]
);
nodes.forEach((node: ESTree.Literal | ESTree.Expression) => {
root = NodeFactory.binaryExpressionNode('+', root, node);
});
// Start from index 2 since we already used 0 and 1
const filteredNodesLength: number = filteredNodes.length;
for (let i: number = 2; i < filteredNodesLength; i++) {
root = NodeFactory.binaryExpressionNode('+', root, filteredNodes[i]);
}
transformedNode = root;
} else {
transformedNode = nodes[0];
transformedNode = filteredNodes[0];
}
NodeUtils.parentizeAst(transformedNode);
@@ -216,8 +216,10 @@ export class BasePropertiesExtractor implements IObjectExpressionExtractor {
objectExpressionNode: ESTree.ObjectExpression,
removablePropertyIds: number[]
): void {
const removablePropertyIdsSet: Set<number> = new Set(removablePropertyIds);
objectExpressionNode.properties = objectExpressionNode.properties.filter(
(property: ESTree.Property | ESTree.SpreadElement, index: number) => !removablePropertyIds.includes(index)
(property: ESTree.Property | ESTree.SpreadElement, index: number) => !removablePropertyIdsSet.has(index)
);
}
}
@@ -127,21 +127,9 @@ export class DirectivePlacementTransformer extends AbstractNodeTransformer {
// append new directive node at the top of lexical scope statements
NodeAppender.prepend(nodeWithLexicalScopeStatements, [newDirectiveNode]);
// remove found directive node
let isDirectiveNodeRemoved: boolean = false;
estraverse.replace(nodeWithLexicalScopeStatements, {
enter: (node: ESTree.Node): estraverse.VisitorOption | undefined => {
if (isDirectiveNodeRemoved) {
return estraverse.VisitorOption.Break;
}
if (node === directiveNode) {
isDirectiveNodeRemoved = true;
return estraverse.VisitorOption.Remove;
}
}
});
nodeWithLexicalScopeStatements.body = nodeWithLexicalScopeStatements.body.filter(
(node) => node !== directiveNode
);
}
return nodeWithLexicalScopeStatements;
@@ -140,15 +140,16 @@ export class StringArrayScopeCallsWrapperTransformer extends AbstractNodeTransfo
const { scopeCallsWrappersData } = stringArrayScopeCallsWrappersData;
const scopeCallsWrappersDataLength: number = scopeCallsWrappersData.length;
const upperStringArrayCallsWrapperData = this.getUpperStringArrayCallsWrapperData(
stringArrayScopeCallsWrappersData
);
/**
* Iterates over each name of scope wrapper name
* Reverse iteration appends wrappers at index `0` at the correct order
*/
for (let i = scopeCallsWrappersDataLength - 1; i >= 0; i--) {
const stringArrayScopeCallsWrapperData = scopeCallsWrappersData[i];
const upperStringArrayCallsWrapperData = this.getUpperStringArrayCallsWrapperData(
stringArrayScopeCallsWrappersData
);
this.getAndAppendStringArrayScopeCallsWrapperNode(
lexicalScopeBodyNode,
@@ -328,18 +328,24 @@ export class StringArrayTransformer extends AbstractNodeTransformer {
const nextScopeCallsWrapperParameterIndexesData: IStringArrayScopeCallsWrapperParameterIndexesData | null =
this.getStringArrayCallsWrapperParameterIndexesData();
stringArrayScopeCallsWrappersDataByEncoding[encoding] = {
encoding,
scopeCallsWrappersData: [
...stringArrayScopeCallsWrappersData,
{
name: nextScopeCallsWrapperName,
index: nextScopeCallsWrapperShiftedIndex,
parameterIndexesData: nextScopeCallsWrapperParameterIndexesData
}
]
const newWrapperData: IStringArrayScopeCallsWrapperData = {
name: nextScopeCallsWrapperName,
index: nextScopeCallsWrapperShiftedIndex,
parameterIndexesData: nextScopeCallsWrapperParameterIndexesData
};
let encodingData = stringArrayScopeCallsWrappersDataByEncoding[encoding];
if (!encodingData) {
encodingData = {
encoding,
scopeCallsWrappersData: [newWrapperData]
};
stringArrayScopeCallsWrappersDataByEncoding[encoding] = encodingData;
} else {
encodingData.scopeCallsWrappersData.push(newWrapperData);
}
this.stringArrayScopeCallsWrappersDataStorage.set(
currentLexicalScopeBodyNode,
stringArrayScopeCallsWrappersDataByEncoding
@@ -78,9 +78,9 @@ export class StringArrayStorage
private readonly rc4Keys: string[];
/**
* @type {Map<string, string[]>}
* @type {Map<string, Set<string>>}
*/
private readonly rc4EncodedValuesSourcesCache: Map<string, string[]> = new Map();
private readonly rc4EncodedValuesSourcesCache: Map<string, Set<string>> = new Map();
/**
* @type {number}
@@ -224,19 +224,13 @@ export class StringArrayStorage
this.storage = new Map(
this.arrayUtils
.shuffle(Array.from(this.storage.entries()))
.map<[`${string}-${TStringArrayEncoding}`, IStringArrayStorageItemData]>(
([value, stringArrayStorageItemData], index: number) => {
stringArrayStorageItemData.index = index;
.map<
[`${string}-${TStringArrayEncoding}`, IStringArrayStorageItemData]
>(([value, stringArrayStorageItemData], index: number) => {
stringArrayStorageItemData.index = index;
return [value, stringArrayStorageItemData];
}
)
.sort(
(
[, stringArrayStorageItemDataA]: [string, IStringArrayStorageItemData],
[, stringArrayStorageItemDataB]: [string, IStringArrayStorageItemData]
) => stringArrayStorageItemDataA.index - stringArrayStorageItemDataB.index
)
return [value, stringArrayStorageItemData];
})
);
}
@@ -296,29 +290,36 @@ export class StringArrayStorage
* if collision will happen, just try to encode value again
*/
case StringArrayEncoding.Rc4: {
const decodeKey: string = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
const encodedValue: string = this.cryptUtilsStringArray.btoa(
this.cryptUtilsStringArray.rc4(value, decodeKey)
);
const maxRetryAttempts: number = 50;
const encodedValueSources: string[] = this.rc4EncodedValuesSourcesCache.get(encodedValue) ?? [];
let encodedValueSourcesLength: number = encodedValueSources.length;
for (let attempt: number = 0; attempt < maxRetryAttempts; attempt++) {
const decodeKey: string = this.randomGenerator.getRandomGenerator().pickone(this.rc4Keys);
const encodedValue: string = this.cryptUtilsStringArray.btoa(
this.cryptUtilsStringArray.rc4(value, decodeKey)
);
const shouldAddValueToSourcesCache: boolean =
!encodedValueSourcesLength || !encodedValueSources.includes(value);
const encodedValueSources: Set<string> =
this.rc4EncodedValuesSourcesCache.get(encodedValue) ?? new Set();
if (shouldAddValueToSourcesCache) {
encodedValueSources.push(value);
encodedValueSourcesLength++;
const shouldAddValueToSourcesCache: boolean =
encodedValueSources.size === 0 || !encodedValueSources.has(value);
if (shouldAddValueToSourcesCache) {
encodedValueSources.add(value);
}
this.rc4EncodedValuesSourcesCache.set(encodedValue, encodedValueSources);
if (encodedValueSources.size <= 1) {
return { encodedValue, encoding, decodeKey };
}
}
this.rc4EncodedValuesSourcesCache.set(encodedValue, encodedValueSources);
if (encodedValueSourcesLength > 1) {
return this.getEncodedValue(value);
}
return { encodedValue, encoding, decodeKey };
return {
encodedValue: this.cryptUtilsStringArray.btoa(value),
encoding: StringArrayEncoding.Base64,
decodeKey: null
};
}
case StringArrayEncoding.Base64: {
+10 -11
View File
@@ -103,7 +103,9 @@ export class ArrayUtils implements IArrayUtils {
* @returns {T[]}
*/
public rotate<T>(array: T[], times: number): T[] {
if (!array.length) {
const arrayLength: number = array.length;
if (!arrayLength) {
throw new ReferenceError('Cannot rotate empty array.');
}
@@ -111,19 +113,16 @@ export class ArrayUtils implements IArrayUtils {
return array;
}
const newArray: T[] = array;
// Normalize rotation amount to avoid unnecessary full rotations
// O(N) algorithm using slice instead of O(N*R) with pop/unshift
const normalizedTimes: number = times % arrayLength;
let temp: T | undefined;
while (times--) {
temp = newArray.pop();
if (temp) {
newArray.unshift(temp);
}
if (normalizedTimes === 0) {
return [...array];
}
return newArray;
// Right rotation: take last `normalizedTimes` elements and put them at the front
return [...array.slice(-normalizedTimes), ...array.slice(0, -normalizedTimes)];
}
/**
+6 -2
View File
@@ -19,6 +19,11 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder {
*/
private static readonly forceEscapeCharactersRegExp: RegExp = /[\x00-\x1F\x7F-\x9F'"\\\s]/;
/**
* @type {RegExp}
*/
private static readonly replaceRegExp: RegExp = /[\s\S]/g;
/**
* @type {Map<string, string>}
*/
@@ -37,12 +42,11 @@ export class EscapeSequenceEncoder implements IEscapeSequenceEncoder {
}
const radix: number = 16;
const replaceRegExp: RegExp = new RegExp('[\\s\\S]', 'g');
let prefix: string;
let template: string;
const result: string = string.replace(replaceRegExp, (character: string): string => {
const result: string = string.replace(EscapeSequenceEncoder.replaceRegExp, (character: string): string => {
const shouldEncodeCharacter: boolean =
encodeAllSymbols || EscapeSequenceEncoder.forceEscapeCharactersRegExp.test(character);