26 examples

Infinite recursion

Recursive function calls itself indefinitely, causing stack overflow.

[ FAQ1 ]

What is infinite recursion?

Infinite recursion occurs when a recursive function lacks a proper base case or termination condition, causing it to repeatedly invoke itself indefinitely. Each recursive call consumes additional space on the call stack, and without termination, the call stack eventually exceeds its limit, triggering a stack overflow error. Infinite recursion commonly arises from logic errors, such as incorrect or missing base cases, poorly defined recursive conditions, or misunderstandings about recursion depth. This issue typically results in immediate application crashes or runtime exceptions.
[ FAQ2 ]

How to fix infinite recursion

To fix infinite recursion, carefully define and implement appropriate base cases in recursive functions, ensuring each recursive call moves closer to this termination condition. Verify recursive logic to confirm that the recursion depth reduces with each call, ultimately reaching a stopping point. If recursion depth is expected to be large, consider refactoring the solution to an iterative approach, which is generally safer and less prone to stack overflows. Utilize debugging tools and stack tracing to identify recursion depth issues early, helping you proactively resolve recursion-related errors.
diff block
return 0
}
-function errorPropertiesFromError(error: Error, metadata?: ErrorMetadata): ErrorProperties {
+function exceptionFromError(error: Error, metadata?: ErrorMetadata): Exception {
const frames = parseStackFrames(error)
const handled = metadata?.handled ?? true
const synthetic = metadata?.synthetic ?? false
const exceptionType = metadata?.overrideExceptionType ? metadata.overrideExceptionType : error.name
const exceptionMessage = extractMessage(error)
+ return {
+ type: exceptionType,
+ value: exceptionMessage,
+ stacktrace: {
+ frames,
+ type: 'raw',
+ },
+ mechanism: {
+ handled,
+ synthetic,
+ },
+ }
+}
+function exceptionListFromError(error: Error, metadata?: ErrorMetadata): ErrorProperties['$exception_list'] {
+ const exception = exceptionFromError(error, metadata)
+ if (error.cause && isError(error.cause)) {
Greptile
greptile
logic: potential infinite recursion if error.cause creates a circular reference
suggested fix
+ if (error.cause && isError(error.cause) && error.cause !== error) {
diff block
package org.owasp.webgoat.container.lessons;
-import lombok.extern.slf4j.Slf4j;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.*;
import org.apache.commons.lang3.ArrayUtils;
import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
import org.owasp.webgoat.container.assignments.AssignmentHints;
import org.owasp.webgoat.container.assignments.AttackResult;
import org.owasp.webgoat.container.session.Course;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.util.CollectionUtils;
+import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
-import java.lang.reflect.Method;
-import java.lang.reflect.ParameterizedType;
-import java.util.*;
-
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toList;
-
-@Slf4j
@Configuration
public class CourseConfiguration {
+ private final List<Lesson> lessons;
+ private final List<AssignmentEndpoint> assignments;
+ private final String contextPath;
- private final List<Lesson> lessons;
- private final List<AssignmentEndpoint> assignments;
- private final Map<String, List<AssignmentEndpoint>> assignmentsByPackage;
+ public CourseConfiguration(
+ List<Lesson> lessons,
+ List<AssignmentEndpoint> assignments,
+ @Value("${server.servlet.context-path}") String contextPath) {
+ this.lessons = lessons;
+ this.assignments = assignments;
+ this.contextPath = contextPath.equals("/") ? "" : contextPath;
+ }
- public CourseConfiguration(List<Lesson> lessons, List<AssignmentEndpoint> assignments) {
- this.lessons = lessons;
- this.assignments = assignments;
- assignmentsByPackage = this.assignments.stream().collect(groupingBy(a -> a.getClass().getPackageName()));
+ private void attachToLessonInParentPackage(
+ AssignmentEndpoint assignmentEndpoint, String packageName) {
+ if (packageName.equals("org.owasp.webgoat.lessons")) {
+ throw new IllegalStateException(
+ "No lesson found for assignment: '%s'"
+ .formatted(assignmentEndpoint.getClass().getSimpleName()));
}
+ lessons.stream()
+ .filter(l -> l.getClass().getPackageName().equals(packageName))
+ .findFirst()
+ .ifPresentOrElse(
+ l -> l.addAssignment(toAssignment(assignmentEndpoint)),
+ () ->
+ attachToLessonInParentPackage(
+ assignmentEndpoint, packageName.substring(0, packageName.lastIndexOf("."))));
Greptile
greptile
logic: Potential infinite recursion if packageName cannot be further split. Add base case for root package.
suggested fix
+ var lastDot = packageName.lastIndexOf(".");
+ if (lastDot == -1) {
throw new IllegalStateException(
+ "No lesson found for assignment: '%s' in any parent package"
.formatted(assignmentEndpoint.getClass().getSimpleName()));
}
attachToLessonInParentPackage(
+ assignmentEndpoint, packageName.substring(0, lastDot));
diff block
+import re
+from typing import Any, Dict, List, Optional
+
+def normalize_app_name(app_name: str) -> str:
+ """
+ Convert app name to a valid Python identifier.
+
+ Args:
+ app_name: The original app name
+
+ Returns:
+ A normalized name suitable for use as a Python identifier
+
+ Examples:
+ "My App" -> "my_app"
+ "Test-ApP" -> "test_app"
+ "123 Service" -> "app_123_service"
+ """
+ # Replace non-alphanumeric chars with underscores
+ normalized = re.sub(r'[^a-zA-Z0-9]+', '_', app_name)
+
+ # Ensure it starts with a letter
+ if not normalized[0].isalpha():
+ normalized = 'app_' + normalized
+
+ # Make it lowercase for consistency
+ return normalized.lower()
+
+
+def to_snake_case(s: str) -> str:
+ """
+ Convert a string to snake_case.
+
+ Args:
+ s: The string to convert
+
+ Returns:
+ The snake_case version of the string
+ """
+ # Remove leading numbers
+ s = re.sub(r"^\d+", "", s)
+
+ # Replace all non-alphanumeric characters with spaces
+ s = re.sub(r"[^a-zA-Z0-9]+", " ", s)
+
+ # Replace spaces with underscores
+ s = re.sub(r"\s+", "_", s)
+
+ # Add underscores between camel case words
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
+ s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)
+
+ # Remove leading and trailing underscores
+ s = s.strip("_")
+
+ return s.lower()
+
+
+def to_title_case(s: str) -> str:
+ """
+ Convert a string to TitleCase.
+
+ Args:
+ s: The string to convert
+
+ Returns:
+ The TitleCase version of the string
+ """
+ # Remove leading numbers
+ s = re.sub(r"^\d+", "", s)
+
+ # Replace all non-alphanumeric characters with underscores
+ s = re.sub(r"[^a-zA-Z0-9]+", "_", s)
+
+ # Replace all underscores with capital letter of next word
+ return re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), s)
+
+
+def infer_type(value: Any) -> Optional[str]:
+ """
+ Infer the Python type of a value for code generation.
+
+ Args:
+ value: The value to infer the type of
+
+ Returns:
+ A string representation of the type, or None if the type can't be inferred
+ """
+ if isinstance(value, str):
+ return "str"
+ elif isinstance(value, bool):
+ return "bool"
+ elif isinstance(value, (int, float)):
+ # The default union mode in pydantic is "smart" mode,
+ # which will use the most specific type possible.
+ return "Union[float, int]"
+ elif isinstance(value, list):
+ if len(value) > 0:
+ return f"list[{infer_type(value[0])}]"
Greptile
greptile
style: Recursive call to infer_type could cause infinite recursion with nested lists. Add depth limit.
diff block
+#low risk
+
+# utils.py
+def add(a, b):
+ return a + b
+
+def reverse_string(s):
+ return s[::-1]
+
+def factorial(n):
+ if n == 0:
+ return 1
+ return n * factorial(n - 1)
Greptile
greptile
logic: No input validation for negative numbers. This will cause infinite recursion for n &lt; 0.
suggested fix
def factorial(n):
+ if n < 0:
+ raise ValueError("Factorial is not defined for negative numbers")
if n == 0:
return 1
return n * factorial(n - 1)
diff block
+import { WorkflowStep } from '@/workflow/types/Workflow';
+
+export const getPreviousSteps = (
+ steps: WorkflowStep[],
+ currentStepId: string,
+): WorkflowStep[] => {
+ const parentSteps = steps.filter((step) =>
+ step.nextStepIds?.includes(currentStepId),
+ );
+
+ const grandParentSteps = parentSteps
+ .map((step) => {
+ return getPreviousSteps(steps, step.id);
+ })
+ .flat();
Greptile
greptile
logic: No protection against circular references in workflow steps. Could cause infinite recursion if steps form a cycle.
suggested fix
+ const visitedSteps = new Set<string>();
const grandParentSteps = parentSteps
.map((step) => {
+ if (visitedSteps.has(step.id)) return [];
+ visitedSteps.add(step.id);
return getPreviousSteps(steps, step.id);
})
.flat();
diff block
const { isDragSelectionStartEnabled } = useDragSelect();
const { DragSelection } = useSelectionContainer({
- shouldStartSelecting: (target) => {
+ shouldStartSelecting: (target: EventTarget | null) => {
if (!isDragSelectionStartEnabled()) {
return false;
}
+
+ if (!(target instanceof Node)) {
+ return false;
+ }
+
+ const hasSelectionBoundary = !!selectionBoundaryRef?.current;
+ if (
+ hasSelectionBoundary &&
+ !selectionBoundaryRef.current.contains(target)
+ ) {
+ return false;
+ }
+
+ if (
+ !hasSelectionBoundary &&
+ !selectableElementsRef.current?.contains(target)
+ ) {
+ return false;
+ }
+
if (target instanceof HTMLElement || target instanceof SVGElement) {
let el = target;
while (el.parentElement && !el.dataset.selectDisable) {
el = el.parentElement;
}
Greptile
greptile
logic: This loop could potentially cause infinite recursion if `el.parentElement` exists but `el.dataset` is undefined
suggested fix
+ while (el.parentElement && el.dataset && !el.dataset.selectDisable) {
+ el = el.parentElement;
}
diff block
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
-import { Repository } from 'typeorm';
-import { IntegrationSetting, RequestContext } from '@gauzy/core';
-import { ID } from '@gauzy/contracts';
-import { UpdateMakeComSettingsDTO } from './dto/update-make-com-settings.dto';
-import { IMakeComIntegrationSettings, MakeSettingName } from './types';
+import { HttpService } from '@nestjs/axios';
+import { catchError, firstValueFrom } from 'rxjs';
+import { AxiosError } from 'axios';
+import {
+ IntegrationSettingService,
+ IntegrationTenantService,
+ RequestContext
+} from '@gauzy/core';
+import { IMakeComIntegrationSettings, MakeSettingName, IntegrationEnum } from '@gauzy/contracts';
+import { MakeComOAuthService } from './make-com-oauth.service';
@Injectable()
export class MakeComService {
private readonly logger = new Logger(MakeComService.name);
constructor(
- @InjectRepository(IntegrationSetting)
- private readonly integrationSettingRepository: Repository<IntegrationSetting>
+ private readonly httpService: HttpService,
+ private readonly integrationSettingService: IntegrationSettingService,
+ private readonly integrationTenantService: IntegrationTenantService,
+ private readonly makeComOAuthService: MakeComOAuthService
) {}
/**
- * Retrieves the Make.com integration settings for a specific tenant.
+ * Retrieves Make.com integration settings for the current tenant.
*
- * @param tenantId - The unique identifier for the tenant.
- * @param organizationId - Optional organization identifier.
- * @returns A promise that resolves to an object containing the integration settings.
+ * @returns {Promise<IMakeComIntegrationSettings>} A promise that resolves to the Make.com integration settings.
*/
- async getIntegrationSettings(organizationId?: ID): Promise<IMakeComIntegrationSettings> {
- const tenantId = RequestContext.currentTenantId();
+ async getIntegrationSettings(): Promise<IMakeComIntegrationSettings> {
+ try {
+ const tenantId = RequestContext.currentTenantId();
+ if (!tenantId) {
+ throw new NotFoundException('Tenant ID not found in request context');
+ }
- if (!tenantId) {
- throw new NotFoundException('Tenant ID not found in request context');
- }
+ // Find the integration for the current tenant
+ const integrationTenant = await this.integrationTenantService.findOneByOptions({
+ where: {
+ name: IntegrationEnum.MakeCom,
+ tenantId
+ },
+ relations: ['settings']
+ });
- try {
- // Fetch both settings concurrently
- const [enabledSetting, webhookUrlSetting] = await Promise.all([
- this.findSetting(tenantId, organizationId, MakeSettingName.IS_ENABLED),
- this.findSetting(tenantId, organizationId, MakeSettingName.WEBHOOK_URL)
- ]);
+ if (!integrationTenant) {
+ return {
+ isEnabled: false,
+ webhookUrl: null
+ };
+ }
+
+ // Extract webhook settings from integration settings
+ const enabledSetting = integrationTenant.settings.find(
+ (setting) => setting.settingsName === MakeSettingName.IS_ENABLED
+ );
+ const webhookUrlSetting = integrationTenant.settings.find(
+ (setting) => setting.settingsName === MakeSettingName.WEBHOOK_URL
+ );
return {
- isEnabled: enabledSetting?.settingsValue === 'true',
- webhookUrl: webhookUrlSetting?.settingsValue ?? null
+ isEnabled: enabledSetting ? enabledSetting.settingsValue === 'true' : false,
+ webhookUrl: webhookUrlSetting ? webhookUrlSetting.settingsValue : null
};
} catch (error) {
- this.logger.warn(`Failed to get Make.com settings: ${error.message}`);
- // Return default values if settings not found
- return {
- isEnabled: false,
- webhookUrl: null
- };
+ this.logger.error('Error retrieving Make.com integration settings:', error);
+ throw error;
}
}
/**
- * Update Make.com integration settings for a tenant.
+ * Updates Make.com integration settings for the current tenant.
*
- * @param input - The DTO containing the updated settings.
- * @param organizationId - Optional organization identifier.
- * @returns A promise that resolves to an object containing the updated settings.
+ * @param {Object} input - The settings to update.
+ * @param {boolean} input.isEnabled - Whether the integration is enabled.
+ * @param {string} input.webhookUrl - The webhook URL for Make.com.
+ * @returns {Promise<IMakeComIntegrationSettings>} A promise that resolves to the updated settings.
*/
- async updateIntegrationSettings(
- input: UpdateMakeComSettingsDTO,
- organizationId?: ID
- ): Promise<IMakeComIntegrationSettings> {
- const tenantId = RequestContext.currentTenantId();
-
- if (!tenantId) {
- throw new NotFoundException('Tenant ID not found in request context');
- }
+ async updateIntegrationSettings(input: {
+ isEnabled?: boolean;
+ webhookUrl?: string;
+ }): Promise<IMakeComIntegrationSettings> {
+ try {
+ const tenantId = RequestContext.currentTenantId();
+ if (!tenantId) {
+ throw new NotFoundException('Tenant ID not found in request context');
+ }
- const webhookUrl = input.webhookUrl ?? null;
- const isEnabled = input.isEnabled;
+ // Find the integration for the current tenant
+ const integrationTenant = await this.integrationTenantService.findOneByOptions({
+ where: {
+ name: IntegrationEnum.MakeCom,
+ tenantId
+ },
+ relations: ['settings']
+ });
+
+ if (!integrationTenant) {
+ throw new NotFoundException(`${IntegrationEnum.MakeCom} integration not found for this tenant`);
+ }
+
+ const updates = [];
+
+ // Update isEnabled setting if provided
+ if (input.isEnabled !== undefined) {
+ const enabledSetting = integrationTenant.settings.find(
+ (setting) => setting.settingsName === MakeSettingName.IS_ENABLED
+ );
+ if (enabledSetting) {
+ enabledSetting.settingsValue = input.isEnabled.toString();
+ updates.push(this.integrationSettingService.create(enabledSetting));
+ }
+ }
- // Update both settings concurrently
- await Promise.all([
- this.upsertSetting(tenantId, organizationId, MakeSettingName.IS_ENABLED, isEnabled.toString()),
- this.upsertSetting(tenantId, organizationId, MakeSettingName.WEBHOOK_URL, webhookUrl)
- ]);
+ // Update webhookUrl setting if provided
+ if (input.webhookUrl !== undefined) {
+ const webhookUrlSetting = integrationTenant.settings.find(
+ (setting) => setting.settingsName === MakeSettingName.WEBHOOK_URL
+ );
+ if (webhookUrlSetting) {
+ webhookUrlSetting.settingsValue = input.webhookUrl;
+ updates.push(this.integrationSettingService.create(webhookUrlSetting));
+ }
+ }
- return {
- isEnabled: input.isEnabled,
- webhookUrl
- };
+ // Wait for all updates to complete
+ await Promise.all(updates);
+
+ // Return the updated settings
+ return this.getIntegrationSettings();
+ } catch (error) {
+ this.logger.error('Error updating Make.com integration settings:', error);
+ throw error;
+ }
}
/**
- * Find a setting by tenant, organization, and name
+ * Makes an authenticated API call to Make.com.
*
- * @param tenantId - The unique identifier for the tenant.
- * @param organizationId - The identifier for the organization or null.
- * @param settingsName - The name of the setting to find.
- * @returns A promise that resolves to an IntegrationSetting or null if not found.
+ * @param {string} makeApiUrl - The Make.com API endpoint.
+ * @param {string} method - The HTTP method to use.
+ * @param {any} data - The data to send with the request.
+ * @returns {Promise<any>} - The API response.
*/
- private async findSetting(
- tenantId: ID,
- organizationId: ID | null,
- settingsName: MakeSettingName
- ): Promise<IntegrationSetting | null> {
- // Construct a typed query object
- const query: Partial<IntegrationSetting> = { tenantId, settingsName };
-
- if (organizationId) {
- query.organizationId = organizationId;
- }
+ async makeApiCall(makeApiUrl: string, method: string = 'GET', data: any = null): Promise<any> {
+ try {
+ const tenantId = RequestContext.currentTenantId();
+ if (!tenantId) {
+ throw new NotFoundException('Tenant ID not found in request context');
+ }
+
+ // Find the integration for the current tenant
+ const integrationTenant = await this.integrationTenantService.findOneByOptions({
+ where: {
+ name: IntegrationEnum.MakeCom,
+ tenantId
+ },
+ relations: ['settings']
+ });
+
+ if (!integrationTenant) {
+ throw new NotFoundException(`${IntegrationEnum.MakeCom} integration not found for this tenant`);
+ }
+
+ // Get the access token
+ const accessTokenSetting = integrationTenant.settings.find(
+ (setting) => setting.settingsName === 'access_token'
+ );
+
+ if (!accessTokenSetting) {
+ throw new NotFoundException('Access token not found for Make.com integration');
+ }
+
+ const headers = {
+ Authorization: `Bearer ${accessTokenSetting.settingsValue}`,
+ 'Content-Type': 'application/json'
+ };
+
+ let response;
+ switch (method.toUpperCase()) {
+ case 'GET':
+ response = await firstValueFrom(
+ this.httpService.get(makeApiUrl, { headers }).pipe(
+ catchError((error: AxiosError) => {
+ // Handle token expiration
+ if (error.response?.status === 401) {
+ // Token might be expired, try refreshing
+ return this.handleTokenRefresh(integrationTenant.id, makeApiUrl, method, data);
+ }
+ throw error;
+ })
+ )
+ );
+ break;
+ case 'POST':
+ response = await firstValueFrom(
+ this.httpService.post(makeApiUrl, data, { headers }).pipe(
+ catchError((error: AxiosError) => {
+ if (error.response?.status === 401) {
+ return this.handleTokenRefresh(integrationTenant.id, makeApiUrl, method, data);
+ }
+ throw error;
+ })
+ )
+ );
+ break;
+ case 'PUT':
+ response = await firstValueFrom(
+ this.httpService.put(makeApiUrl, data, { headers }).pipe(
+ catchError((error: AxiosError) => {
+ if (error.response?.status === 401) {
+ return this.handleTokenRefresh(integrationTenant.id, makeApiUrl, method, data);
+ }
+ throw error;
+ })
+ )
+ );
+ break;
+ case 'DELETE':
+ response = await firstValueFrom(
+ this.httpService.delete(makeApiUrl, { headers }).pipe(
+ catchError((error: AxiosError) => {
+ if (error.response?.status === 401) {
+ return this.handleTokenRefresh(integrationTenant.id, makeApiUrl, method, data);
+ }
+ throw error;
+ })
+ )
+ );
+ break;
+ default:
+ throw new Error(`Unsupported HTTP method: ${method}`);
+ }
- return this.integrationSettingRepository.findOne({ where: query });
+ return response.data;
+ } catch (error) {
+ this.logger.error(`Error making API call to Make.com: ${error.message}`, error.stack);
+ throw error;
+ }
}
/**
- * Upsert a Make.com integration setting for a tenant.
+ * Handles token refresh when an API call fails due to token expiration.
*
- * If the setting exists, its value is updated. If it does not exist, a new setting is created.
- *
- * @param tenantId - The unique identifier for the tenant.
- * @param organizationId - The unique identifier for the organization (optional).
- * @param settingsName - The name of the setting to update or create.
- * @param settingsValue - The new value for the setting.
- * @returns A promise that resolves to the updated or created IntegrationSetting.
+ * @param {string} integrationId - The integration ID.
+ * @param {string} makeApiUrl - The makeApiUrl to retry after token refresh.
+ * @param {string} method - The HTTP method to use.
+ * @param {any} data - The data to send with the request.
+ * @returns {Promise<any>} - The API response after token refresh.
*/
- private async upsertSetting(
- tenantId: ID,
- organizationId: ID | null,
- settingsName: MakeSettingName,
- settingsValue: string
- ): Promise<IntegrationSetting> {
- let setting = await this.findSetting(tenantId, organizationId, settingsName);
-
- if (setting) {
- // Update the existing setting value
- setting.settingsValue = settingsValue;
- } else {
- // Create a new setting with the provided values
- setting = this.integrationSettingRepository.create({
- tenantId,
- organizationId: organizationId ?? null,
- settingsName,
- settingsValue
- });
- }
+ private async handleTokenRefresh(
+ integrationId: string,
+ makeApiUrl: string,
+ method: string,
+ data: any
+ ): Promise<any> {
+ try {
+ // Refresh the token
+ await this.makeComOAuthService.refreshToken(integrationId);
- return this.integrationSettingRepository.save(setting);
+ // Retry the API call with the new token
+ return this.makeApiCall(makeApiUrl, method, data);
Greptile
greptile
logic: Potential infinite recursion if token refresh fails repeatedly. Consider adding a retry limit
diff block
maxAuthCodes: Number.parseInt(process.env.GAUZY_ZAPIER_MAX_AUTH_CODES) || 1000,
instanceCount: process.env.GAUZY_ZAPIER_INSTANCE_COUNT === 'true',
redirectUri:
- process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/zapier/callback`,
+ process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integration/zapier/oauth/callback`,
postInstallUrl:
process.env.GAUZY_ZAPIER_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/zapier`
},
+ makeCom: {
+ clientId: process.env.GAUZY_MAKE_CLIENT_ID,
+ clientSecret: process.env.GAUZY_MAKE_CLIENT_SECRET,
+ webhookUrl: process.env.GAUZY_MAKE_WEBHOOK_URL || `${process.env.GAUZY_MAKE_WEBHOOK_URL}`,
Greptile
greptile
logic: Circular reference in fallback value - `process.env.GAUZY_MAKE_WEBHOOK_URL || ${process.env.GAUZY_MAKE_WEBHOOK_URL}` will cause infinite recursion. Should likely be `process.env.GAUZY_MAKE_WEBHOOK_URL || ${process.env.API_BASE_URL}/api/integration/make/webhook` ```suggestion + webhookUrl: process.env.GAUZY_MAKE_WEBHOOK_URL || `${process.env.API_BASE_URL}/api/integration/make/webhook`, ```
diff block
+import { isClassInstance } from './is-class-instance';
+import { isObject } from './is-object';
+
+/**
+ * Recursively parses an object, applying a callback to all non-object leaf values.
+ *
+ * @param source - The object to parse.
+ * @param callback - The function to apply to each primitive value.
+ * @returns The modified object.
+ */
+export function parseObject<T extends object>(source: T, callback: (value: any) => any): T {
+ if (!isObject(source)) {
+ return source;
+ }
+
+ for (const key of Object.keys(source)) {
+ const value = source[key];
+
+ if (isObject(value)) {
+ if (!isClassInstance(value)) {
+ source[key] = parseObject(value, callback);
+ }
Greptile
greptile
logic: No protection against circular references. Could cause infinite recursion.
diff block
+import { NamedTriggerGroup, NamedTrigger, Result } from "../types";
+import { getPreferenceValues } from "@raycast/api";
+import { createJXAScript } from "../constants";
+import { runAppleScript } from "@raycast/utils";
+import { returnErrorText, isErrorResponse } from "./utils";
+
+function flattenGroups(groups: NamedTriggerGroup[]): Record<string, string> {
+ // Create a map for quick lookup of groups by UUID
+ const groupMap: Record<string, NamedTriggerGroup> = {};
+ groups.forEach((group) => {
+ groupMap[group.uuid] = group;
+ });
+
+ // Function to build the full path for a group
+ function buildGroupPath(group: NamedTriggerGroup): string {
+ if (!group.parentUUID) {
+ return group.name;
+ }
+
+ const parent = groupMap[group.parentUUID];
+ if (!parent) {
+ return group.name;
+ }
+
+ return `${buildGroupPath(parent)} > ${group.name}`;
+ }
Greptile
greptile
logic: Potential infinite recursion if there's a circular dependency in group parentUUID references ```suggestion + function buildGroupPath(group: NamedTriggerGroup, visited = new Set<string>()): string { if (!group.parentUUID) { return group.name; } + if (visited.has(group.uuid)) { + return group.name; // Break circular dependency } + visited.add(group.uuid); const parent = groupMap[group.parentUUID]; if (!parent) { return group.name; } + return `${buildGroupPath(parent, visited)} > ${group.name}`; } ```
diff block
paths: allCycles,
}
}
+
+/**
+ * Convert UI loop block to executor Loop format
+ *
+ * @param loopBlockId - ID of the loop block to convert
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Loop object for execution engine or undefined if not a valid loop
+ */
+export function convertLoopBlockToLoop(
+ loopBlockId: string,
+ blocks: Record<string, BlockState>
+): Loop | undefined {
+ const loopBlock = blocks[loopBlockId];
+ if (!loopBlock || loopBlock.type !== 'loop') return undefined;
+
+ return {
+ id: loopBlockId,
+ nodes: findChildNodes(loopBlockId, blocks),
+ iterations: loopBlock.data?.count || 5,
+ loopType: loopBlock.data?.loopType || 'for',
+ forEachItems: loopBlock.data?.collection || '',
+ };
+}
+
+/**
+ * Find all nodes that are children of this loop
+ *
+ * @param loopId - ID of the loop to find children for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are direct children of this loop
+ */
+export function findChildNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
+ return Object.values(blocks)
+ .filter(block => block.data?.parentId === loopId)
+ .map(block => block.id);
+}
+
+/**
+ * Find all descendant nodes, including children, grandchildren, etc.
+ *
+ * @param loopId - ID of the loop to find descendants for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are descendants of this loop
+ */
+export function findAllDescendantNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
+ const descendants: string[] = [];
+ const findDescendants = (parentId: string) => {
+ const children = Object.values(blocks)
+ .filter(block => block.data?.parentId === parentId)
+ .map(block => block.id);
+
+ children.forEach(childId => {
+ descendants.push(childId);
+ findDescendants(childId);
+ });
+ };
+
+ findDescendants(loopId);
+ return descendants;
+}
Greptile
greptile
logic: potential infinite recursion if there's a circular parent-child relationship in the blocks
suggested fix
export function findAllDescendantNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
const descendants: string[] = [];
+ const visited = new Set<string>();
const findDescendants = (parentId: string) => {
+ if (visited.has(parentId)) return;
+ visited.add(parentId);
const children = Object.values(blocks)
.filter(block => block.data?.parentId === parentId)
.map(block => block.id);
children.forEach(childId => {
descendants.push(childId);
findDescendants(childId);
});
};
findDescendants(loopId);
return descendants;
}
diff block
paths: allCycles,
}
}
+
+/**
+ * Convert UI loop block to executor Loop format
+ *
+ * @param loopBlockId - ID of the loop block to convert
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Loop object for execution engine or undefined if not a valid loop
+ */
+export function convertLoopBlockToLoop(
+ loopBlockId: string,
+ blocks: Record<string, BlockState>
+): Loop | undefined {
+ const loopBlock = blocks[loopBlockId];
+ if (!loopBlock || loopBlock.type !== 'loop') return undefined;
+
+ return {
+ id: loopBlockId,
+ nodes: findChildNodes(loopBlockId, blocks),
+ iterations: loopBlock.data?.count || 5,
+ loopType: loopBlock.data?.loopType || 'for',
+ forEachItems: loopBlock.data?.collection || '',
+ };
+}
+
+/**
+ * Find all nodes that are children of this loop
+ *
+ * @param loopId - ID of the loop to find children for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are direct children of this loop
+ */
+export function findChildNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
+ return Object.values(blocks)
+ .filter(block => block.data?.parentId === loopId)
+ .map(block => block.id);
+}
+
+/**
+ * Find all descendant nodes, including children, grandchildren, etc.
+ *
+ * @param loopId - ID of the loop to find descendants for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are descendants of this loop
+ */
+export function findAllDescendantNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
+ const descendants: string[] = [];
+ const findDescendants = (parentId: string) => {
+ const children = Object.values(blocks)
+ .filter(block => block.data?.parentId === parentId)
+ .map(block => block.id);
+
+ children.forEach(childId => {
+ descendants.push(childId);
+ findDescendants(childId);
+ });
+ };
+
+ findDescendants(loopId);
+ return descendants;
+}
Greptile
greptile
logic: No protection against infinite recursion if blocks form a circular parent-child relationship. Consider adding a visited set.
suggested fix
export function findAllDescendantNodes(loopId: string, blocks: Record<string, BlockState>): string[] {
const descendants: string[] = [];
+ const visited = new Set<string>();
const findDescendants = (parentId: string) => {
+ if (visited.has(parentId)) return;
+ visited.add(parentId);
const children = Object.values(blocks)
.filter(block => block.data?.parentId === parentId)
.map(block => block.id);
children.forEach(childId => {
descendants.push(childId);
findDescendants(childId);
});
};
findDescendants(loopId);
return descendants;
}
diff block
return extension ? `${filePrefix}.${extension}` : null;
};
-export const getConfigInfo = (packageJson: PackageJson, configDir?: string) => {
+export const getConfigInfo = (configDir?: string) => {
let storybookConfigDir = configDir ?? '.storybook';
- const storybookScript = packageJson.scripts?.storybook;
- if (storybookScript && !configDir) {
- const configParam = getStorybookConfiguration(storybookScript, '-c', '--config-dir');
- if (configParam) {
- storybookConfigDir = configParam;
+ if (!existsSync(storybookConfigDir)) {
+ const packageJsonPaths = JsPackageManager.listAllPackageJsonPaths(storybookConfigDir);
Greptile
greptile
logic: Potential infinite recursion - listAllPackageJsonPaths is called with storybookConfigDir which may not be a valid root directory
diff block
paths: allCycles,
}
}
+
+/**
+ * Convert UI loop block to executor Loop format
+ *
+ * @param loopBlockId - ID of the loop block to convert
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Loop object for execution engine or undefined if not a valid loop
+ */
+export function convertLoopBlockToLoop(
+ loopBlockId: string,
+ blocks: Record<string, BlockState>
+): Loop | undefined {
+ const loopBlock = blocks[loopBlockId];
+ if (!loopBlock || loopBlock.type !== 'loop') return undefined;
+
+ return {
+ id: loopBlockId,
+ nodes: findChildNodes(loopBlockId, blocks),
+ iterations: loopBlock.data?.count || 5,
+ loopType: loopBlock.data?.loopType || 'for',
+ forEachItems: loopBlock.data?.collection || '',
+ };
+}
+
+/**
+ * Convert UI parallel block to executor Parallel format
+ *
+ * @param parallelBlockId - ID of the parallel block to convert
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Parallel object for execution engine or undefined if not a valid parallel block
+ */
+export function convertParallelBlockToParallel(
+ parallelBlockId: string,
+ blocks: Record<string, BlockState>
+): Parallel | undefined {
+ const parallelBlock = blocks[parallelBlockId];
+ if (!parallelBlock || parallelBlock.type !== 'parallel') return undefined;
+
+ return {
+ id: parallelBlockId,
+ nodes: findChildNodes(parallelBlockId, blocks),
+ distribution: parallelBlock.data?.collection || '',
+ };
+}
+
+/**
+ * Find all nodes that are children of this container (loop or parallel)
+ *
+ * @param containerId - ID of the container to find children for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are direct children of this container
+ */
+export function findChildNodes(containerId: string, blocks: Record<string, BlockState>): string[] {
+ return Object.values(blocks)
+ .filter(block => block.data?.parentId === containerId)
+ .map(block => block.id);
+}
+
+/**
+ * Find all descendant nodes, including children, grandchildren, etc.
+ *
+ * @param containerId - ID of the container to find descendants for
+ * @param blocks - Record of all blocks in the workflow
+ * @returns Array of node IDs that are descendants of this container
+ */
+export function findAllDescendantNodes(containerId: string, blocks: Record<string, BlockState>): string[] {
+ const descendants: string[] = [];
+ const findDescendants = (parentId: string) => {
+ const children = Object.values(blocks)
+ .filter(block => block.data?.parentId === parentId)
+ .map(block => block.id);
+
+ children.forEach(childId => {
+ descendants.push(childId);
+ findDescendants(childId);
+ });
Greptile
greptile
logic: Potential infinite recursion if circular parent references exist in blocks
suggested fix
descendants.push(childId);
+ if (!descendants.includes(childId)) { // Prevent infinite recursion
findDescendants(childId);
}
});
diff block
+import { type JsPackageManager, syncStorybookAddons } from 'storybook/internal/common';
+
+import picocolors from 'picocolors';
+import prompts from 'prompts';
+import { dedent } from 'ts-dedent';
+
+import { runCodemod } from '../automigrate/codemod';
+import { getFrameworkPackageName } from '../automigrate/helpers/mainConfigFile';
+import type { CommandFix } from '../automigrate/types';
+import { printBoxedMessage } from '../util';
+import { configToCsfFactory } from './helpers/config-to-csf-factory';
+import { storyToCsfFactory } from './helpers/story-to-csf-factory';
+
+export const logger = console;
+
+async function runStoriesCodemod(options: {
+ dryRun: boolean | undefined;
+ packageManager: JsPackageManager;
+ useSubPathImports: boolean;
+ previewConfigPath: string;
+}) {
+ const { dryRun, packageManager, ...codemodOptions } = options;
+ try {
+ let globString = 'src/**/*.stories.*';
+ if (!process.env.IN_STORYBOOK_SANDBOX) {
+ logger.log('Please enter the glob for your stories to migrate');
+ globString = (
+ await prompts(
+ {
+ type: 'text',
+ name: 'glob',
+ message: 'glob',
+ initial: globString,
+ },
+ {
+ onCancel: () => process.exit(0),
+ }
+ )
+ ).glob;
+ }
+
+ logger.log('\n🛠️ Applying codemod on your stories, this might take some time...');
+
+ // TODO: Move the csf-2-to-3 codemod into automigrations
+ await packageManager.executeCommand({
+ command: `${packageManager.getRemoteRunCommand()} storybook migrate csf-2-to-3 --glob=${globString}`,
+ args: [],
+ stdio: 'ignore',
+ ignoreError: true,
+ });
+
+ await runCodemod(globString, (info) => storyToCsfFactory(info, codemodOptions), {
+ dryRun,
+ });
+ } catch (err: any) {
+ if (err.message === 'No files matched') {
+ await runStoriesCodemod(options);
+ } else {
Greptile
greptile
logic: Recursively calling runStoriesCodemod on 'No files matched' could lead to infinite recursion if the glob pattern is consistently invalid
diff block
sameSite: 'strict'
});
};
+
+export const easeInOutCubic = (t: number): number => {
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+};
+
+export const parseWidthValue = (width: string): { value: number; unit: 'px' | '%' } => {
+ const match = width.match(/^(\d+(?:\.\d+)?)(px|%)$/);
+ if (!match) throw new Error('Invalid width format. Must be in px or %');
+ return {
+ value: parseFloat(match[1]),
+ unit: match[2] as 'px' | '%'
+ };
+};
+
+export const convertPxToPercentage = (px: number, containerWidth: number): number => {
+ return (px / containerWidth) * 100;
+};
+
+export const getCurrentSizePercentage = (
+ size: string | number,
+ otherSize: string | number,
+ container: HTMLElement
+): number => {
+ if (size === 'auto') {
+ // If this side is auto, calculate based on the other side
+ const otherPercentage = getCurrentSizePercentage(otherSize, size, container);
Greptile
greptile
logic: potential infinite recursion if both sizes are 'auto'
diff block
+import { ComponentFactoryResolver, ElementRef, Component } from '@angular/core';
+
+@Component({
+ standalone: false,
+ selector: 'storybook-multiple-selector, storybook-multiple-selector2',
+ template: `<h3>Multiple selector</h3>
+Selector: {{ selectors }} <br />
+Generated template: {{ generatedTemplate }}`,
+})
+export class MultipleSelectorComponent {
+ generatedTemplate!: string;
+
+ selectors!: string;
+
+ constructor(
+ public el: ElementRef,
+ private resolver: ComponentFactoryResolver
+ ) {
+ const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent);
+ this.selectors = factory.selector;
+ this.generatedTemplate = el.nativeElement.outerHTML;
+ }
+}
+
+@Component({
+ standalone: false,
+ selector: 'storybook-button, button[foo], .button[foo], button[baz]',
+ template: `<h3>Multiple selector</h3>
+Selector: {{ selectors }} <br />
+Generated template: {{ generatedTemplate }}`,
+})
+export class MultipleClassSelectorComponent {
+ generatedTemplate!: string;
+
+ selectors!: string;
+
+ constructor(
+ public el: ElementRef,
+ private resolver: ComponentFactoryResolver
+ ) {
+ const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent);
Greptile
greptile
logic: resolveComponentFactory is called with MultipleClassSelectorComponent instead of the component's own class, causing infinite recursion
diff block
+import { Clipboard, showHUD } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
+/**
+ *
+ * "Hello World" -> "Hello_world"
+ */
+export default async function main(props: { arguments: { prefix?: string } }) {
+ let input: string | undefined;
+ try {
+ input = await Clipboard.readText();
+ if (!input) {
+ await showHUD("No text in clipboard");
+ return;
+ }
+ const result = input.split(" ").filter(Boolean).join("_");
+ const resultWithPrefix = props.arguments.prefix ? `${prefixValue(props.arguments.prefix)}${result}` : result;
+ await Clipboard.copy(resultWithPrefix);
+ await showHUD(resultWithPrefix);
+ } catch (error) {
+ await showFailureToast("Failed to read clipboard", {
+ title: "Failed to read clipboard",
+ message: "Please try again",
+ primaryAction: {
+ title: "Try again",
+ onAction: () => {
+ main(props);
+ },
Greptile
greptile
logic: Recursive call to main() in error handler could cause infinite recursion if clipboard keeps failing
diff block
+/**
+ * Client for communicating with a Harmony Hub.
+ * Handles connection, command execution, activity management, and state caching.
+ * @module
+ */
+
+import getHarmonyClient from "@harmonyhub/client-ws";
+import { getPreferenceValues, LocalStorage } from "@raycast/api";
+
+import { HarmonyError, ErrorCategory } from "../../types/core/errors";
+import {
+ HarmonyHub,
+ HarmonyDevice,
+ HarmonyActivity,
+ HarmonyCommand,
+ isHarmonyDevice,
+ isHarmonyActivity,
+} from "../../types/core/harmony";
+import { debug, error, info, warn } from "../logger";
+
+/** Cache expiration time in milliseconds (24 hours) */
+const CACHE_EXPIRY = 24 * 60 * 60 * 1000;
+
+/**
+ * Interface for cached hub configuration
+ * @interface CachedConfig
+ */
+interface CachedConfig {
+ /** List of devices associated with the hub */
+ devices: HarmonyDevice[];
+ /** List of activities configured on the hub */
+ activities: HarmonyActivity[];
+ /** Timestamp when the cache was created */
+ timestamp: number;
+}
+
+/**
+ * Interface for command execution body
+ * @interface HarmonyCommandBody
+ */
+interface HarmonyCommandBody {
+ /** Command identifier */
+ command: string;
+ /** Target device identifier */
+ deviceId: string;
+ /** Command type (e.g., "IRCommand") */
+ type: string;
+}
+
+/**
+ * Interface for command function configuration
+ * @interface CommandFunction
+ */
+interface CommandFunction {
+ /** Function name */
+ name: string;
+ /** Display label */
+ label?: string;
+ /** Command action configuration */
+ action?: {
+ /** Command identifier */
+ command?: string;
+ };
+}
+
+/**
+ * Interface for device control group
+ * @interface ControlGroup
+ */
+interface ControlGroup {
+ /** Group name */
+ name: string;
+ /** List of functions in this group */
+ function: CommandFunction[];
+}
+
+/**
+ * Interface for raw device data from hub
+ * @interface RawDevice
+ */
+interface RawDevice {
+ /** Device identifier */
+ id: string;
+ /** Display label */
+ label?: string;
+ /** Device type */
+ type?: string;
+ /** List of control groups */
+ controlGroup: ControlGroup[];
+}
+
+/**
+ * Interface for raw hub configuration
+ * @interface RawConfig
+ */
+interface RawConfig {
+ /** List of devices */
+ device: RawDevice[];
+}
+
+/**
+ * Interface for raw activity data from hub
+ * @interface RawActivity
+ */
+interface RawActivity {
+ /** Activity identifier */
+ id: string;
+ /** Activity display label */
+ label: string;
+ /** Activity type */
+ type: string;
+}
+
+/**
+ * Client for communicating with a Harmony Hub
+ * Handles connection, command execution, and activity management
+ */
+export class HarmonyClient {
+ /** Map of active clients by hub ID */
+ private static activeClients: Map<string, HarmonyClient> = new Map();
+
+ /** Get or create a client for a hub */
+ public static getClient(hub: HarmonyHub): HarmonyClient {
+ const existingClient = this.activeClients.get(hub.hubId);
+ if (existingClient) {
+ debug(`Reusing existing client for hub ${hub.name}`);
+ return existingClient;
+ }
+
+ debug(`Creating new client for hub ${hub.name}`);
+ const client = new HarmonyClient(hub);
+ this.activeClients.set(hub.hubId, client);
+ return client;
+ }
+
+ /** Connected client instance */
+ private client: Awaited<ReturnType<typeof getHarmonyClient>> | null = null;
+ /** Connection state */
+ private isConnected = false;
+ /** The hub this client is connected to */
+ public readonly hub: HarmonyHub;
+ /** Cache key for this hub's configuration */
+ private cacheKey: string;
+
+ /**
+ * Creates a new HarmonyClient instance
+ * @param hub - The Harmony Hub to connect to
+ */
+ private constructor(hub: HarmonyHub) {
+ this.hub = hub;
+ this.cacheKey = `harmony-config-${hub.hubId}`;
+ }
+
+ /**
+ * Check if the client is currently connected
+ * @returns True if connected, false otherwise
+ */
+ public isClientConnected(): boolean {
+ return this.isConnected;
+ }
+
+ /**
+ * Connects to the Harmony Hub and retrieves its configuration.
+ * Establishes WebSocket connection and verifies connectivity by fetching initial config.
+ * Sets up disconnect handler and validates connection state.
+ * @throws {HarmonyError} If connection fails or initial config cannot be retrieved
+ */
+ public async connect(): Promise<void> {
+ if (this.isConnected) {
+ info(`Already connected to hub ${this.hub.name}`);
+ return;
+ }
+
+ try {
+ info(`Initiating connection to hub ${this.hub.name} (${this.hub.ip})`);
+
+ // Create client with remoteId if available for faster connection
+ debug("Creating Harmony client", {
+ ip: this.hub.ip,
+ port: this.hub.port,
+ remoteId: this.hub.remoteId,
+ });
+
+ this.client = await getHarmonyClient(this.hub.ip);
+
+ // Setup disconnect handler before setting connected state
+ this.client?.on("disconnected", () => {
+ warn(`Disconnected from hub ${this.hub.name}`);
+ this.isConnected = false;
+ // Remove from active clients on disconnect
+ HarmonyClient.activeClients.delete(this.hub.hubId);
+ });
+
+ // Verify connection by attempting to get config
+ debug("Verifying connection by fetching initial config");
+
+ // Load both devices and activities to ensure proper caching
+ const [devices, activities] = await Promise.all([this.getDevicesFromHub(), this.getActivitiesFromHub()]);
+
+ // Only set connected state after successful verification
+ this.isConnected = true;
+ info(`Successfully connected to hub ${this.hub.name}`);
+
+ // Cache the complete config
+ await this.updateConfigCache(devices, activities);
+
+ debug("Initial config loaded and cached", {
+ hubName: this.hub.name,
+ deviceCount: devices.length,
+ activityCount: activities.length,
+ });
+
+ return;
+ } catch (err) {
+ this.isConnected = false;
+ this.client = null;
+ // Remove from active clients on error
+ HarmonyClient.activeClients.delete(this.hub.hubId);
+ const harmonyError = new HarmonyError(
+ "Failed to connect to hub",
+ ErrorCategory.HUB_COMMUNICATION,
+ err instanceof Error ? err : undefined
+ );
+ error("Connection failed", { error: harmonyError.getDetailedMessage() });
+ throw harmonyError;
+ }
+ }
+
+ /**
+ * Retrieves the list of devices from the hub.
+ * Attempts to load from cache first, falls back to hub query if cache is invalid.
+ * @returns Promise resolving to list of devices
+ * @throws {HarmonyError} If retrieving devices fails or hub is not connected
+ */
+ public async getDevices(): Promise<HarmonyDevice[]> {
+ if (!this.client || !this.isConnected) {
+ throw new HarmonyError("Not connected to hub", ErrorCategory.STATE);
+ }
+
+ try {
+ // Try to get from cache first
+ const cached = await this.getCachedConfig();
+ if (cached?.devices) {
+ debug("Using cached devices for hub", {
+ hubName: this.hub.name,
+ deviceCount: cached.devices.length,
+ });
+ return cached.devices;
+ }
+
+ debug("No cached devices found, fetching from hub");
+ const devices = await this.getDevicesFromHub();
+
+ // Cache the new devices
+ await this.updateConfigCache(devices, await this.getActivitiesFromHub());
+
+ return devices;
+ } catch (err) {
+ throw new HarmonyError(
+ "Failed to get devices",
+ ErrorCategory.HUB_COMMUNICATION,
+ err instanceof Error ? err : undefined
+ );
+ }
+ }
+
+ /**
+ * Gets devices directly from the hub via WebSocket.
+ * @returns Promise resolving to list of mapped HarmonyDevice objects
+ * @throws {HarmonyError} If client not initialized or hub communication fails
+ * @private
+ */
+ private async getDevicesFromHub(): Promise<HarmonyDevice[]> {
+ if (!this.client) {
+ throw new HarmonyError("Client not initialized", ErrorCategory.CONNECTION);
+ }
+
+ try {
+ debug("Fetching devices from hub", { hubName: this.hub.name });
+ const rawConfig = (await this.client.getAvailableCommands()) as RawConfig;
+
+ if (!rawConfig.device || rawConfig.device.length === 0) {
+ warn("No devices found in hub config");
+ return [];
+ }
+
+ debug(`Found ${rawConfig.device.length} devices`);
+
+ const mappedDevices = rawConfig.device.map((device) => {
+ const mappedDevice = {
+ id: device.id,
+ name: device.label || device.id,
+ type: device.type || "Unknown",
+ commands: device.controlGroup.flatMap((group) =>
+ group.function.map((fn) => ({
+ id: fn.name,
+ name: fn.name,
+ label: fn.label || fn.name,
+ deviceId: device.id,
+ group: fn.action?.command || "IRCommand",
+ }))
+ ),
+ } as HarmonyDevice;
+
+ // Validate mapped device
+ if (!isHarmonyDevice(mappedDevice)) {
+ error("Invalid device mapping", { device, mappedDevice });
+ throw new HarmonyError(`Invalid device mapping for ${device.id}`, ErrorCategory.VALIDATION);
+ }
+
+ return mappedDevice;
+ });
+
+ debug("Successfully mapped devices", {
+ deviceCount: mappedDevices.length,
+ commandCounts: mappedDevices.map((d) => ({
+ deviceId: d.id,
+ commandCount: d.commands.length,
+ })),
+ });
+
+ return mappedDevices;
+ } catch (err) {
+ throw new HarmonyError(
+ "Failed to get devices from hub",
+ ErrorCategory.HUB_COMMUNICATION,
+ err instanceof Error ? err : new Error(String(err))
+ );
+ }
+ }
+
+ /**
+ * Retrieves the list of activities from the hub.
+ * Attempts to load from cache first, falls back to hub query if cache is invalid.
+ * @returns Promise resolving to list of activities
+ * @throws {HarmonyError} If retrieving activities fails or hub is not connected
+ */
+ public async getActivities(): Promise<HarmonyActivity[]> {
+ if (!this.client || !this.isConnected) {
+ throw new HarmonyError("Not connected to hub", ErrorCategory.STATE);
+ }
+
+ try {
+ // Try to get from cache first
+ const cached = await this.getCachedConfig();
+ if (cached?.activities) {
+ debug("Using cached activities for hub", {
+ hubName: this.hub.name,
+ activityCount: cached.activities.length,
+ });
+ return cached.activities;
+ }
+
+ debug("No cached activities found, fetching from hub", {
+ hubName: this.hub.name,
+ });
+ const activities = await this.getActivitiesFromHub();
+
+ // Cache the new activities along with current devices
+ await this.updateConfigCache(await this.getDevices(), activities);
+
+ return activities;
+ } catch (err) {
+ throw new HarmonyError(
+ "Failed to get activities",
+ ErrorCategory.HUB_COMMUNICATION,
+ err instanceof Error ? err : undefined
+ );
+ }
+ }
+
+ /**
+ * Gets the currently running activity.
+ * Queries the hub for current activity and matches it against known activities.
+ * @returns Promise resolving to current activity or null if none
+ * @throws {HarmonyError} If retrieving current activity fails or hub is not connected
+ */
+ public async getCurrentActivity(): Promise<HarmonyActivity | null> {
+ if (!this.client || !this.isConnected) {
+ throw new HarmonyError("Not connected to hub", ErrorCategory.STATE);
+ }
+
+ try {
+ const rawActivity = await this.client.getCurrentActivity();
+ debug("Got current activity from hub", { rawActivity });
+
+ if (!rawActivity) {
+ return null;
+ }
+
+ // Convert raw activity string to proper type
+ const currentActivityId = String(rawActivity);
+
+ // Get all activities to find the current one
+ const activities = await this.getActivities();
+ const activity = activities.find((a) => a.id === currentActivityId);
+
+ if (!activity) {
+ warn("Current activity not found in activity list", {
+ currentActivityId,
+ availableActivities: activities.map((a) => a.id),
+ });
+ return null;
+ }
+
+ const currentActivity = {
+ ...activity,
+ isCurrent: true,
+ };
+
+ // Validate current activity
+ if (!isHarmonyActivity(currentActivity)) {
+ error("Invalid current activity", { currentActivity });
+ throw new HarmonyError("Invalid current activity data", ErrorCategory.VALIDATION);
+ }
+
+ return currentActivity;
+ } catch (err) {
+ throw new HarmonyError(
+ "Failed to get current activity",
+ ErrorCategory.HUB_COMMUNICATION,
+ err instanceof Error ? err : new Error(String(err))
+ );
+ }
+ }
+
+ /**
+ * Starts an activity by ID.
+ * Initiates the activity and waits for confirmation of successful start.
+ * @param activityId - ID of the activity to start
+ * @throws {HarmonyError} If starting activity fails or hub is not connected
+ */
+ public async startActivity(activityId: string): Promise<void> {
+ if (!this.client || !this.isConnected) {
+ throw new HarmonyError("Not connected to hub", ErrorCategory.STATE);
+ }
+
+ try {
+ debug("Starting activity", { activityId });
+
+ await this.client.startActivity(activityId);
+
+ // Wait for activity to start and verify
+ const startTime = Date.now();
+ const maxWaitTime = 10000; // 10 seconds max wait
+
+ while (Date.now() - startTime < maxWaitTime) {
+ const currentActivity = await this.getCurrentActivity();
+ if (currentActivity?.id === activityId) {
+ debug("Activity started successfully", { activityId });
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
+ throw new Error("Timeout waiting for activity to start");
+ } catch (err) {
+ throw new HarmonyError(
+ `Failed to start activity ${activityId}`,
+ ErrorCategory.COMMAND_EXECUTION,
+ err instanceof Error ? err : undefined
+ );
+ }
+ }
+
+ /**
+ * Stops the current activity.
+ * Sends stop command and waits for confirmation of successful stop.
+ * @throws {HarmonyError} If stopping activity fails or hub is not connected
+ */
+ public async stopActivity(): Promise<void> {
+ if (!this.client || !this.isConnected) {
+ throw new HarmonyError("Not connected to hub", ErrorCategory.STATE);
+ }
+
+ try {
+ debug("Stopping current activity");
+
+ const currentActivity = await this.getCurrentActivity();
+ if (!currentActivity) {
+ debug("No activity running");
+ return;
+ }
+
+ await this.client.turnOff();
+
+ // Wait for activity to stop and verify
+ const startTime = Date.now();
+ const maxWaitTime = 10000; // 10 seconds max wait
+
+ while (Date.now() - startTime < maxWaitTime) {
+ const activity = await this.getCurrentActivity();
+ if (!activity) {
+ debug("Activity stopped successfully");
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
+ throw new Error("Timeout waiting for activity to stop");
+ } catch (err) {
+ throw new HarmonyError(
+ "Failed to stop activity",
+ ErrorCategory.COMMAND_EXECUTION,
+ err instanceof Error ? err : undefined
+ );
+ }
+ }
+
+ /**
+ * Gets cached configuration if available.
+ * Checks cache validity and expiration.
+ * @returns Promise resolving to cached configuration or null
+ * @throws {HarmonyError} If reading cache fails
+ * @private
+ */
+ private async getCachedConfig(): Promise<CachedConfig | null> {
+ try {
+ const cached = await LocalStorage.getItem<string>(this.cacheKey);
+ if (!cached) {
+ debug("No cache found for hub", { hubName: this.hub.name });
+ return null;
+ }
+
+ const config = JSON.parse(cached) as CachedConfig;
+
+ // Validate cache has required data
+ if (!config.devices?.length || !config.activities?.length) {
+ debug("Cache invalid - missing data", {
+ hubName: this.hub.name,
+ hasDevices: !!config.devices?.length,
+ hasActivities: !!config.activities?.length,
+ });
+ await LocalStorage.removeItem(this.cacheKey);
+ return null;
+ }
+
+ // Check if cache is expired
+ if (Date.now() - config.timestamp > CACHE_EXPIRY) {
+ info("Config cache expired for hub", this.hub.name);
+ await LocalStorage.removeItem(this.cacheKey);
+ return null;
+ }
+
+ debug("Using valid cache for hub", {
+ hubName: this.hub.name,
+ deviceCount: config.devices.length,
+ activityCount: config.activities.length,
+ age: Math.round((Date.now() - config.timestamp) / 1000) + "s",
+ });
+
+ return config;
+ } catch (err) {
+ warn("Failed to get cached config:", err);
+ return null;
+ }
+ }
+
+ /**
+ * Update the config cache with new devices and activities.
+ * @param devices - List of devices to cache
+ * @param activities - List of activities to cache
+ * @private
+ */
+ private async updateConfigCache(devices: HarmonyDevice[], activities: HarmonyActivity[]): Promise<void> {
+ try {
+ // Validate we have data to cache
+ if (!devices.length && !activities.length) {
+ debug("Skipping cache update - no data to cache", {
+ hubName: this.hub.name,
+ });
+ return;
+ }
+
+ const cache: CachedConfig = {
+ devices: devices.length ? devices : await this.getDevices(),
+ activities: activities.length ? activities : await this.getActivitiesFromHub(),
+ timestamp: Date.now(),
Greptile
greptile
logic: Potential circular dependency issue here. If devices.length is 0, this calls getDevices() which might call getActivitiesFromHub() again, which could lead to infinite recursion in error cases
diff block
+package com.egardia.sunrise.exception;
+
+import com.egardia.sunrise.entity.SunriseOrder;
+import com.egardia.sunrise.entity.SunriseOrderResponse;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+@Getter
+@EqualsAndHashCode(callSuper = false)
+public class InvalidOrderException extends RuntimeException {
+
+ private final String error;
+ private final SunriseOrder order;
+ private final SunriseOrderResponse.Status status; // Use existing Status enum
+
+ public InvalidOrderException(SunriseOrder order, SunriseOrderResponse.Status status,
+ String error) {
+ super("Invalid order: " + order + ", status: " + status + ", error: " + error);
Greptile
greptile
logic: calling toString() on order object in exception message could cause infinite recursion if order.toString references this exception
diff block
return isUniversalGroupFilterLike(filterOrGroup) ? (
<div className="w-full">
<UniversalFilters.Group key={index} index={index} group={filterOrGroup}>
- <div className="flex items-center gap-2 border-t py-4">
- {(totalFiltersCount ?? 0) > 0 && (
+ <div className={showAddFilter ? 'flex items-center gap-2 border-t py-4' : 'pt-2'}>
+ {(totalFiltersCount ?? 0) > 0 && showAddFilter && (
<span className="font-semibold">Applied filters:</span>
)}
<RecordingsUniversalFilterGroup size={size} totalFiltersCount={totalFiltersCount} />
Greptile
greptile
logic: Potential infinite recursion - RecordingsUniversalFilterGroup is calling itself without passing showAddFilter prop
suggested fix
+ <RecordingsUniversalFilterGroup size={size} totalFiltersCount={totalFiltersCount} showAddFilter={false} />
diff block
return self
def get_class_probabilities(self, text: str, temperature: float = 1.0) -> torch.Tensor:
- inputs = self.tokenizer(text, return_tensors="pt", padding=True,
- truncation=True, max_length=512).to(self.device)
- with torch.no_grad():
- logits = self.model(**inputs).logits
- scaled_logits = logits / temperature
- return softmax(scaled_logits, dim=-1)
+ # Split text into chunks of approximately equal size
+ max_chunk_length = 512
+ words = text.split()
+ n_chunks = min(
+ 200, (len(words) + max_chunk_length - 1) // max_chunk_length)
+ chunk_size = (len(words) + n_chunks - 1) // n_chunks
+
+ chunks = [' '.join(words[i:i + chunk_size])
+ for i in range(0, len(words), chunk_size)]
+ all_probabilities = []
+
+ for chunk in chunks:
+ print(f"Processing chunk: {chunk}")
+ inputs = self.tokenizer(chunk, return_tensors="pt", padding=True,
+ truncation=True, max_length=512).to(self.device)
+ with torch.no_grad():
+ logits = self.model(**inputs).logits
+ scaled_logits = logits / temperature
+ all_probabilities.append(softmax(scaled_logits, dim=-1))
+
+ # Aggregate probabilities by taking the maximum across all chunks
+ if all_probabilities:
+ stacked_probs = torch.stack(all_probabilities)
+ return torch.max(stacked_probs, dim=0)[0]
+ else:
+ # Handle empty text case
+ return self.get_class_probabilities("", temperature)
Greptile
greptile
logic: Potential infinite recursion if empty string is passed - will keep calling itself with empty string
suggested fix
+ # Return zero probabilities for empty text
+ return torch.zeros((1, self.model.config.num_labels), device=self.device)
diff block
+/*
+ * This file is part of WebGoat, an Open Web Application Security Project utility. For details,
+ * please see http://www.owasp.org/
+ * <p>
+ * Copyright (c) 2002 - 2017 Bruce Mayhew
+ * <p>
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
+ * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License along with this program; if
+ * not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ * <p>
+ * Getting Source ==============
+ * <p>
+ * Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository for free software
+ * projects.
+ * <p>
+ */
+
+package org.owasp.webgoat.server;
+
+import lombok.extern.slf4j.Slf4j;
+import org.owasp.webgoat.container.WebGoat;
+import org.owasp.webgoat.webwolf.WebWolf;
+import org.springframework.boot.Banner;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.util.SocketUtils;
+
+import java.lang.reflect.Method;
+
+import static java.util.Optional.of;
+import static java.util.Optional.ofNullable;
+
+@Slf4j
+public class StartWebGoat {
+
+ public static final String WEBGOAT_PORT = "webgoat.port";
+ public static final String WEBWOLF_PORT = "webwolf.port";
+
+ private static final int MAX_PORT = 9999;
+
+ public static void main(String[] args) {
+ setEnvironmentVariableForPort(WEBGOAT_PORT, "8080");
+ setEnvironmentVariableForPort(WEBWOLF_PORT, "9090");
+
+ new SpringApplicationBuilder().parent(ParentConfig.class)
+ .web(WebApplicationType.NONE).bannerMode(Banner.Mode.OFF)
+ .child(WebGoat.class)
+ .web(WebApplicationType.SERVLET)
+ .sibling(WebWolf.class).bannerMode(Banner.Mode.OFF)
+ .web(WebApplicationType.SERVLET)
+ .run(args);
+ }
+
+ private static void setEnvironmentVariableForPort(String name, String defaultValue) {
+ ofNullable(System.getProperty(name))
+ .or(() -> of(defaultValue))
+ .map(Integer::parseInt)
+ .map(port -> findPort(port))
+ .ifPresent(port -> System.setProperty(name, port));
+ }
+
+ public static String findPort(int port) {
+ try {
+ if (port == MAX_PORT) {
+ log.error("No free port found from 8080 - {}", MAX_PORT);
+ return "" + port;
+ }
+ return "" + SocketUtils.findAvailableTcpPort(port, port);
+ } catch (IllegalStateException var4) {
+ return findPort(port + 1);
+ }
Greptile
greptile
logic: Potential infinite recursion if no ports are available between current port and MAX_PORT
diff block
};
};
+const messageToText = (message: Message): string => {
+ let text = "";
+ message.contentArray?.forEach((message) => {
+ text += messageToText(message).trim();
+ });
Greptile
greptile
logic: Potential infinite recursion if contentArray contains circular references. Need to add depth limit or cycle detection.
diff block
return response
}
+export const removeUndefinedAndNull = (obj: any): any => {
+ if (Array.isArray(obj)) {
+ return obj.map(removeUndefinedAndNull)
+ } else if (obj && typeof obj === 'object') {
+ return Object.entries(obj).reduce((acc, [key, value]) => {
+ if (value !== undefined && value !== null) {
+ acc[key] = removeUndefinedAndNull(value)
+ }
+ return acc
+ }, {} as Record<string, any>)
+ }
+ return obj
+}
Greptile
greptile
logic: Potential infinite recursion if cyclic objects are passed; consider adding cycle detection.
suggested fix
+export const removeUndefinedAndNull = (obj: any, visited: WeakSet<object> = new WeakSet()): any => {
if (Array.isArray(obj)) {
+ return obj.map(item => removeUndefinedAndNull(item, visited))
} else if (obj && typeof obj === 'object') {
+ if (visited.has(obj)) {
+ return obj // Break the cycle
}
+ visited.add(obj)
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value !== undefined && value !== null) {
+ acc[key] = removeUndefinedAndNull(value, visited)
}
return acc
}, {} as Record<string, any>)
}
return obj
}
diff block
sameSite: 'strict'
});
};
+
+export const easeInOutCubic = (t: number): number => {
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+};
+
+export const parseWidthValue = (width: string): { value: number; unit: 'px' | '%' } => {
+ const match = width.match(/^(\d+(?:\.\d+)?)(px|%)$/);
+ if (!match) throw new Error('Invalid width format. Must be in px or %');
+ return {
+ value: parseFloat(match[1]),
+ unit: match[2] as 'px' | '%'
+ };
+};
+
+export const convertPxToPercentage = (px: number, containerWidth: number): number => {
+ return (px / containerWidth) * 100;
+};
+
+export const getCurrentSizePercentage = (
+ size: string | number,
+ otherSize: string | number,
+ container: HTMLElement
+): number => {
+ if (size === 'auto') {
+ // If this side is auto, calculate based on the other side
+ const otherPercentage = getCurrentSizePercentage(otherSize, size, container);
+ return 100 - otherPercentage;
+ }
Greptile
greptile
potential infinite recursion if both sizes are 'auto'
suggested fix
if (size === 'auto') {
// If this side is auto, calculate based on the other side
+ if (otherSize === 'auto') {
+ return 50; // Default to 50-50 split if both sides are auto
}
const otherPercentage = getCurrentSizePercentage(otherSize, size, container);
return 100 - otherPercentage;
}