Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"sentry-cli/no-namespace-import": "error",
"sentry-cli/no-examples-in-full-description": "error",
"sentry-cli/no-regex-markdown-parsing": "error",
"sentry-cli/prefer-array-from": "error",
"no-restricted-imports": [
"error",
{
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,10 @@ import type { SentryContext } from "../../context.js";
import { getAuthToken } from "../../lib/db/auth.js";
```

### Copying Iterables

Use `Array.from(value)` to turn a `Set`, `Map` view, iterator, or array into a new array. Keep spread for building arrays from several parts, such as `[...a, ...b]` or `[...items, tail]`. The `sentry-cli/prefer-array-from` lint rule flags `[...value]` and fixes it with `pnpm run lint:fix`.

### Target Resolution

`parseOrgProjectArg()` owns target syntax: a value without `/` is a
Expand Down
37 changes: 37 additions & 0 deletions packages/cli/lint-rules/cli-oxlint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ function regexSource(node) {
return "";
}

const PREFER_ARRAY_FROM_MESSAGE =
"Use Array.from(value) instead of [...value] to copy an iterable.";

const rules = {
"no-silent-catch": rule(
"Handle or report caught errors instead of discarding them.",
Expand Down Expand Up @@ -409,6 +412,40 @@ const rules = {
}
: {},
),
"prefer-array-from": {
meta: {
type: "suggestion",
docs: { description: PREFER_ARRAY_FROM_MESSAGE },
fixable: "code",
schema: [],
},
create(context) {
const filename = context.filename.replaceAll("\\", "/");
if (!filename.includes("/packages/cli/")) return {};
return {
// Only a lone spread is a pure conversion; `[...a, ...b]` and
// `[...a, b]` build new arrays and stay as they are.
ArrayExpression(node) {
const [element] = node.elements;
if (node.elements.length !== 1 || element?.type !== "SpreadElement")
return;
context.report({
node,
message: PREFER_ARRAY_FROM_MESSAGE,
fix(fixer) {
const value = context.sourceCode.getText(element.argument);
// `[...(a, b)]` must not become `Array.from(a, b)`.
const wrapped =
element.argument.type === "SequenceExpression"
? `(${value})`
: value;
return fixer.replaceText(node, `Array.from(${wrapped})`);
},
});
},
};
},
},
"no-skipped-tests": rule(
"Do not leave disabled tests in the suite.",
(_context, file, report) =>
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/alert/issues/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@ async function resolveWebUrl(
throw new ContextError("Organization and project", USAGE_HINT);
}

const orgs = [...new Set(targets.map((t) => t.org))];
const orgs = Array.from(new Set(targets.map((t) => t.org)));
if (orgs.length !== 1) {
throw new ValidationError(
"--web resolved alert rules in multiple organizations. Specify an explicit <org>/ or <org>/<project> target.",
"target",
);
}

const projects = [...new Set(targets.map((t) => t.project))];
const projects = Array.from(new Set(targets.map((t) => t.project)));
return buildIssueAlertsUrl(
// oxlint-disable-next-line typescript/no-non-null-assertion -- orgs length is checked above
orgs[0]!,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/alert/issues/rule-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export async function resolveIssueAlertRule(
);
}

const uniqueNames = [...new Set(allRuleNames)];
const uniqueNames = Array.from(new Set(allRuleNames));
const similar = fuzzyMatch(ref, uniqueNames, { maxResults: 5 });
if (similar.length > 0) {
const lines = similar.map((n) => ` ${n}`).join("\n");
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/commands/alert/metrics/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async function resolveOrgs(
projectSearchResolution,
});
return {
orgs: [...new Set(targets.map((t: ResolvedTarget) => t.org))],
orgs: Array.from(new Set(targets.map((t: ResolvedTarget) => t.org))),
footer,
};
}
Expand All @@ -215,7 +215,7 @@ async function resolveWebUrl(
}

const { orgs } = await resolveOrgs(parsed, cwd, projectSearchResolution);
const uniqueOrgs = [...new Set(orgs)];
const uniqueOrgs = Array.from(new Set(orgs));
if (uniqueOrgs.length === 0) {
throw new ContextError("Organization", USAGE_HINT);
}
Expand Down Expand Up @@ -249,10 +249,10 @@ async function handleResolvedOrgs(
throw new ContextError("Organization", USAGE_HINT);
}

const uniqueOrgs = [...new Set(resolved)];
const uniqueOrgs = Array.from(new Set(resolved));

const contextKey = buildMultiOrgContextKey(uniqueOrgs, flags.query);
const sortedOrgKeys = [...uniqueOrgs].sort();
const sortedOrgKeys = Array.from(uniqueOrgs).sort();

const { cursor: rawCursor, direction } = resolveCursor(
flags.cursor,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/alert/metrics/rule-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export async function resolveMetricAlertRule(
);
}

const uniqueNames = [...new Set(allNames)];
const uniqueNames = Array.from(new Set(allNames));
const similar = fuzzyMatch(ref, uniqueNames, { maxResults: 5 });
if (similar.length > 0) {
const lines = similar.map((n) => ` ${n}`).join("\n");
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/alert/mutation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export function validateMetricTimeWindow(timeWindow: number): void {
return;
}
throw new ValidationError(
`timeWindow must be one of: ${[...METRIC_TIME_WINDOWS].join(", ")} minutes.`,
`timeWindow must be one of: ${Array.from(METRIC_TIME_WINDOWS).join(", ")} minutes.`,
"timeWindow",
);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,9 @@ export function parseFieldKey(key: string): string[] {
// Extract bracket contents: "[name][age]" -> ["name", "age"]
// Empty brackets [] result in empty string "" for array push
const segments: string[] = brackets
? [...brackets.matchAll(BRACKET_CONTENTS_REGEX)].map((m) => m[1] ?? "")
? Array.from(brackets.matchAll(BRACKET_CONTENTS_REGEX)).map(
(m) => m[1] ?? "",
)
: [];

return [baseKey, ...segments];
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export const logoutCommand = buildCommand({
* active one never picks an account that is about to be removed too.
*/
async function removeAccounts(accounts: AccountSummary[]): Promise<void> {
const ordered = [...accounts].sort(
const ordered = Array.from(accounts).sort(
(a, b) => Number(a.active) - Number(b.active),
);
for (const account of ordered) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function resolveRefreshScope(flags: RefreshFlags): string {
);
}
if (flags.scope?.length) {
const scopes = [...flags.scope].flatMap((v) => v.split(","));
const scopes = Array.from(flags.scope).flatMap((v) => v.split(","));
return resolveOAuthScopeString({ scopes });
}
return resolveOAuthScopeString({ readOnly: true });
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dashboard/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ function warnUnknownAggregateFields(
`Aggregate field "${inner}" in "${agg}" is not a known aggregatable span field. ` +
"Span attributes (custom tags) cannot be used with numeric aggregates — " +
"use them in --where or --group-by instead. " +
`Known numeric fields: ${[...KNOWN_SPAN_AGGREGATE_FIELDS].join(", ")}`,
`Known numeric fields: ${Array.from(KNOWN_SPAN_AGGREGATE_FIELDS).join(", ")}`,
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/debug-files/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function formatFindResult(data: FindResult): string {
/** Resolve the requested types, validating each against the known set. */
function resolveTypes(flags: FindFlags): string[] {
if (!(flags.type && flags.type.length > 0)) {
return [...FIND_DIF_TYPES];
return Array.from(FIND_DIF_TYPES);
}
const types = flags.type.map((t) => t.trim().toLowerCase());
for (const type of types) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dsn/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async function listForResolvedProjects<
}),
);
const { pages, failures } = collectProjectResults(results, activeTargets);
const allItems = [...pages.values()].flatMap((page) => page.items);
const allItems = Array.from(pages.values()).flatMap((page) => page.items);
const items = trimWithGroupGuarantee(allItems, flags.limit, targetKey);
const trimmed = items.length < allItems.length;
const cursors = sortedKeys.map((key) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ export const exploreCommand = buildListCommand("explore", {
let fieldList =
flags["time-series"] !== undefined
? ["count()"]
: [...defaultFieldsForDataset(dataset)];
: Array.from(defaultFieldsForDataset(dataset));
if (userSuppliedFields) {
fieldList = flags.field;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/issue/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,7 @@ function formatDetailWithSuggestions(
detailLines: string[],
suggestions: string[],
): string {
const lines = [...detailLines];
const lines = Array.from(detailLines);
if (lines.length > 0) {
lines.push("");
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/issue/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const issueIdsPositional = {
* @returns Normalized issue identifiers
*/
export function collectIssueArgs(args: readonly string[]): string[] {
return [...new Set(args.flatMap(splitNewlineArg))];
return Array.from(new Set(args.flatMap(splitNewlineArg)));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/local/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ export const serverCommand = buildCommand({
printLocalViewerHint(url, flags.format === "json");
await openLocalUiIfRequested(flags.open, url);
if (activeFilters.size > 0) {
logger.info(`Filtering: ${[...activeFilters].join(", ")}`);
logger.info(`Filtering: ${Array.from(activeFilters).join(", ")}`);
}
logger.info("Press Ctrl-C to stop.");

Expand Down Expand Up @@ -1243,7 +1243,7 @@ export const serverCommand = buildCommand({
` Or run: ${bold(`sentry local run -p ${boundPort} -- <your-command>`)}`,
);
if (activeFilters.size > 0) {
logger.info(` Filtering: ${[...activeFilters].join(", ")}`);
logger.info(` Filtering: ${Array.from(activeFilters).join(", ")}`);
}
if (useJson) {
logger.info(" Output: JSON (NDJSON)");
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/local/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function createLocalSessionStore({
if (byId) {
return byId;
}
const matches = [...sessions.values()].filter(
const matches = Array.from(sessions.values()).filter(
(session) => session.label === reference,
);
if (matches.length === 0) {
Expand Down Expand Up @@ -219,13 +219,13 @@ export function createLocalSessionStore({
};
},
list() {
return [...sessions.values()]
return Array.from(sessions.values())
.map(toPublicSession)
.sort((left, right) => right.lastActivityAt - left.lastActivityAt);
},
pruneExpired() {
const timestamp = now();
const expired = [...sessions.values()]
const expired = Array.from(sessions.values())
.filter(
(session) =>
session.state === "retained" &&
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/log/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ async function* generateFollowLogs<T extends LogLike>(
// Initial fetch
const initialLogs = await config.fetch("1m");
if (initialLogs.length > 0) {
yield [...initialLogs].reverse();
yield Array.from(initialLogs).reverse();
}
lastTimestamp = maxTimestamp(initialLogs) ?? lastTimestamp;
config.onInitialLogs?.(initialLogs);
Expand All @@ -405,7 +405,7 @@ async function* generateFollowLogs<T extends LogLike>(

const newLogs = await fetchPoll(config, lastTimestamp);
if (newLogs && newLogs.length > 0) {
yield [...newLogs].reverse();
yield Array.from(newLogs).reverse();
lastTimestamp = maxTimestamp(newLogs) ?? lastTimestamp;
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/project/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function buildPlatformError(nameArg: string, platform?: string): string {
}
}

const platformTable = renderPlatformGrid([...COMMON_PLATFORMS]);
const platformTable = renderPlatformGrid(Array.from(COMMON_PLATFORMS));

return (
`${heading}\n` +
Expand Down Expand Up @@ -483,7 +483,7 @@ function parseProjectSpecs(rawSpecs: readonly string[]): {
);
if (orgs.size > 1) {
throw new ValidationError(
`Cannot create projects across multiple organizations (${[...orgs].join(", ")}).\n\n` +
`Cannot create projects across multiple organizations (${Array.from(orgs).join(", ")}).\n\n` +
"All names must belong to the same org.",
"organization",
);
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/project/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,9 @@ async function resolveOrgsForAutoDetect(cwd: string): Promise<OrgResolution> {
if (targetsResult.ok) {
const { targets, footer, skippedSelfHosted } = targetsResult.value;
if (targets.length > 0) {
const uniqueOrgs = [
...new Set(targets.map((t: ResolvedTarget) => t.org)),
];
const uniqueOrgs = Array.from(
new Set(targets.map((t: ResolvedTarget) => t.org)),
);
return { orgs: uniqueOrgs, footer, skippedSelfHosted };
}
return { orgs: [], skippedSelfHosted };
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/release/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ export const listCommand = buildListCommand("release", {
const parsed = parseOrgProjectArg(target);
// Flatten: -e prod,dev -e staging → ["prod", "dev", "staging"]
const envFilter = flags.environment
? [...flags.environment]
? Array.from(flags.environment)
.flatMap((v) => v.split(","))
.map((s) => s.trim())
.filter(Boolean)
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/replay/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export const listCommand = buildListCommand("replay", {
() =>
listReplays(resolved.org, {
environment,
fields: [...REPLAY_LIST_FIELDS],
fields: Array.from(REPLAY_LIST_FIELDS),
limit: flags.limit,
query,
projectSlugs: resolved.project ? [resolved.project] : undefined,
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ function pathNotFoundError(
): never {
const samePath = findEndpointsByPath(query.path);
if (query.method && samePath.length > 0) {
const methods = [...new Set(samePath.map((endpoint) => endpoint.method))]
const methods = Array.from(
new Set(samePath.map((endpoint) => endpoint.method)),
)
.sort()
.join(", ");
const example =
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/sourcemap/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function computeCommonPrefix(paths: string[]): string {
if (paths.length === 0) {
return "";
}
const sorted = [...paths].sort();
const sorted = Array.from(paths).sort();
const first = sorted[0];
const last = sorted.at(-1);
if (!(first && last)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/trial/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function deduplicateTrials(entries: TrialListEntry[]): TrialListEntry[] {
byName.set(entry.name, entry);
}
}
return [...byName.values()];
return Array.from(byName.values());
}

/**
Expand Down
Loading
Loading