Repository: Azure/azure-sdk-for-net
File: eng/packages/http-client-csharp-mgmt/emitter/src/resource-metadata.ts (TypeScript)

Code before:
        }
      }

      // If no parent or parent not found, move to non-resource methods
      if (!movedToParent) {
        for (const method of resource.metadata.methods) {
          nonResourceMethods.push({
            methodId: method.methodId,
            operationPath: method.operationPath,
            operationScope: method.operationScope
          });
        }
      }
      continue;
    }
    filteredResources.push(resource);
  }

  // Re-sort methods in resources that may have received additional methods from filtered resources
  for (const resource of filteredResources) {
    sortResourceMethods(resource.metadata.methods);
  }

  return filteredResources;
}

/**
 * Assigns non-resource methods to resources based on operationPath prefix matching.
 * If a non-resource method's operationPath has a prefix that matches a resource's
 * resourceIdPattern, the method is moved to that resource as an Action.
 * If multiple resources match, the one with the longest prefix (most segments) wins.
 *
 * @param resources - The list of valid resources
 * @param nonResourceMethods - The array of non-resource methods (will be mutated: matched methods are removed)
 */
export function assignNonResourceMethodsToResources(
  resources: ArmResourceSchema[],
  nonResourceMethods: NonResourceMethod[]
): void {
  const methodsToRemove = new Set<string>();

  for (const method of nonResourceMethods) {
    let bestMatch: ArmResourceSchema | undefined;
    let bestMatchSegmentCount = 0;

    for (const resource of resources) {
      const pattern = resource.metadata.resourceIdPattern;
      if (!pattern) continue;

      // Check if the resource's resourceIdPattern is a proper prefix of the method's operationPath
      if (

Review comment (bug):
The two-pass approach (model-aware first in `processMethod`, then model-agnostic fallback here) is architecturally sound.

However, the "iterate candidates → check `isPrefix` → pick longest match" pattern is repeated in **4 places** across the emitter:

1. **This new function** — manual loop + `getSharedSegmentCount`
2. **`postProcessArmResources()` Step 4** (resource-metadata.ts) — manual loop + **`string.length`** comparison (inconsistent with segment count used here)
3. **`processMethod()` prefix matching** (resource-detection.ts) — manual loop + manual segment counting
4. **Parent-child detection** (resource-detection.ts) — manual loop + `getSharedSegmentCount`

**Suggestion**: Extract a generic `findLongestPrefixMatch<T>` utility in `utils.ts`:

```typescript
export function findLongestPrefixMatch<T>(
  targetPath: string,
  candidates: T[],
  getPath: (candidate: T) => string | undefined,
  properPrefix: boolean = false
): T | undefined {
  let bestMatch: T | undefined;
  let bestSegmentCount = 0;

  for (const candidate of candidates) {
    const candidatePath = getPath(candidate);
    if (!candidatePath) continue;
    if (!isPrefix(candidatePath, targetPath)) continue;
    if (properPrefix && isPrefix(targetPath, candidatePath)) continue;

    const segmentCount = getSharedSegmentCount(candidatePath, targetPath);
    if (segmentCount > bestSegmentCount) {
      bestSegmentCount = segmentCount;
      bestMatch = candidate;
    }
  }
  return bestMatch;
}
```

Then this call site becomes:
```typescript
const bestMatch = findLongestPrefixMatch(
  method.operationPath, resources, r => r.metadata.resourceIdPattern, true
);
```

**Bonus**: This also fixes Step 4's subtle inconsistency — it uses `string.length` (`candidates.reduce((a,b) => a.length > b.length ? a : b)`) which could diverge from segment-count comparison in edge cases.

Code after:
        }
      }
      continue;
    }
    filteredResources.push(resource);
  }

  // Re-sort methods in resources that may have received additional methods from filtered resources
  for (const resource of filteredResources) {
    sortResourceMethods(resource.metadata.methods);
  }

  return filteredResources;
}

/**
 * Assigns non-resource methods to resources based on operationPath prefix matching.
 * If a non-resource method's operationPath has a prefix that matches a resource's
 * resourceIdPattern, the method is moved to that resource as an Action.
 * If multiple resources match, the one with the longest prefix (most segments) wins.
 *
 * @param resources - The list of valid resources
 * @param nonResourceMethods - The array of non-resource methods (will be mutated: matched methods are removed)
 */
export function assignNonResourceMethodsToResources(
  resources: ArmResourceSchema[],
  nonResourceMethods: NonResourceMethod[]
): void {
  const methodsToRemove = new Set<string>();

  for (const method of nonResourceMethods) {
    const bestMatch = findLongestPrefixMatch(
      method.operationPath,
      resources,
      (r) => r.metadata.resourceIdPattern || undefined,
      true
    );

    if (bestMatch) {
      bestMatch.metadata.methods.push({
        methodId: method.methodId,
        kind: ResourceOperationKind.Action,
        operationPath: method.operationPath,
        operationScope: method.operationScope,
        resourceScope: bestMatch.metadata.resourceIdPattern
      });
      methodsToRemove.add(method.methodId);
    }
  }

  // Remove matched methods from non-resource methods array