refactor: mark immutable array parameters as readonly

This commit is contained in:
Simon Chan 2025-06-16 16:32:49 +08:00
parent 40a60ca112
commit 6873c03a9d
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
24 changed files with 89 additions and 60 deletions

View file

@ -35,7 +35,7 @@ export const AdbDefaultInterfaceFilter = {
} as const satisfies UsbInterfaceFilter; } as const satisfies UsbInterfaceFilter;
export function mergeDefaultAdbInterfaceFilter( export function mergeDefaultAdbInterfaceFilter(
filters: USBDeviceFilter[] | undefined, filters: readonly USBDeviceFilter[] | undefined,
): (USBDeviceFilter & UsbInterfaceFilter)[] { ): (USBDeviceFilter & UsbInterfaceFilter)[] {
if (!filters || filters.length === 0) { if (!filters || filters.length === 0) {
return [AdbDefaultInterfaceFilter]; return [AdbDefaultInterfaceFilter];

View file

@ -7,8 +7,8 @@ import { isErrorName, matchFilters } from "./utils.js";
export namespace AdbDaemonWebUsbDeviceManager { export namespace AdbDaemonWebUsbDeviceManager {
export interface RequestDeviceOptions { export interface RequestDeviceOptions {
filters?: USBDeviceFilter[] | undefined; filters?: readonly USBDeviceFilter[] | undefined;
exclusionFilters?: USBDeviceFilter[] | undefined; exclusionFilters?: readonly USBDeviceFilter[] | undefined;
} }
} }
@ -44,7 +44,7 @@ export class AdbDaemonWebUsbDeviceManager {
try { try {
const device = await this.#usbManager.requestDevice({ const device = await this.#usbManager.requestDevice({
filters, filters,
exclusionFilters: options.exclusionFilters, exclusionFilters: options.exclusionFilters as USBDeviceFilter[],
}); });
const interface_ = matchFilters( const interface_ = matchFilters(

View file

@ -25,8 +25,8 @@ export class AdbDaemonWebUsbDeviceObserver
return new AdbDaemonWebUsbDeviceObserver(usb, devices, options); return new AdbDaemonWebUsbDeviceObserver(usb, devices, options);
} }
readonly #filters: (USBDeviceFilter & UsbInterfaceFilter)[]; readonly #filters: readonly (USBDeviceFilter & UsbInterfaceFilter)[];
readonly #exclusionFilters?: USBDeviceFilter[] | undefined; readonly #exclusionFilters?: readonly USBDeviceFilter[] | undefined;
readonly #usbManager: USB; readonly #usbManager: USB;
readonly #onDeviceAdd = new EventEmitter< readonly #onDeviceAdd = new EventEmitter<

View file

@ -80,7 +80,7 @@ export function getSerialNumber(device: USBDevice) {
* *
* ADB interface only has two endpoints, one for input and one for output. * ADB interface only has two endpoints, one for input and one for output.
*/ */
export function findUsbEndpoints(endpoints: USBEndpoint[]) { export function findUsbEndpoints(endpoints: readonly USBEndpoint[]) {
if (endpoints.length === 0) { if (endpoints.length === 0) {
throw new TypeError("No endpoints given"); throw new TypeError("No endpoints given");
} }
@ -153,18 +153,18 @@ export function matchFilter(
export function matchFilters( export function matchFilters(
device: USBDevice, device: USBDevice,
filters: (USBDeviceFilter & UsbInterfaceFilter)[], filters: readonly (USBDeviceFilter & UsbInterfaceFilter)[],
exclusionFilters?: USBDeviceFilter[], exclusionFilters?: readonly USBDeviceFilter[],
): UsbInterfaceIdentifier | false; ): UsbInterfaceIdentifier | false;
export function matchFilters( export function matchFilters(
device: USBDevice, device: USBDevice,
filters: USBDeviceFilter[], filters: readonly USBDeviceFilter[],
exclusionFilters?: USBDeviceFilter[], exclusionFilters?: readonly USBDeviceFilter[],
): boolean; ): boolean;
export function matchFilters( export function matchFilters(
device: USBDevice, device: USBDevice,
filters: USBDeviceFilter[], filters: readonly USBDeviceFilter[],
exclusionFilters?: USBDeviceFilter[], exclusionFilters?: readonly USBDeviceFilter[],
): UsbInterfaceIdentifier | boolean { ): UsbInterfaceIdentifier | boolean {
if (exclusionFilters && exclusionFilters.length > 0) { if (exclusionFilters && exclusionFilters.length > 0) {
if (matchFilters(device, exclusionFilters)) { if (matchFilters(device, exclusionFilters)) {

View file

@ -57,9 +57,9 @@ function concatStreams<T>(...streams: ReadableStream<T>[]): ReadableStream<T> {
} }
export class AdbScrcpyExitedError extends Error { export class AdbScrcpyExitedError extends Error {
output: string[]; output: readonly string[];
constructor(output: string[]) { constructor(output: readonly string[]) {
super("scrcpy server exited prematurely"); super("scrcpy server exited prematurely");
this.output = output; this.output = output;
} }

View file

@ -132,7 +132,7 @@ export class Adb implements Closeable {
} }
rm( rm(
filenames: string | string[], filenames: string | readonly string[],
options?: { recursive?: boolean; force?: boolean }, options?: { recursive?: boolean; force?: boolean },
): Promise<string> { ): Promise<string> {
const args = ["rm"]; const args = ["rm"];
@ -144,10 +144,12 @@ export class Adb implements Closeable {
} }
if (Array.isArray(filenames)) { if (Array.isArray(filenames)) {
for (const filename of filenames) { for (const filename of filenames) {
args.push(escapeArg(filename)); // https://github.com/microsoft/typescript/issues/17002
args.push(escapeArg(filename as string));
} }
} else { } else {
args.push(escapeArg(filenames)); // https://github.com/microsoft/typescript/issues/17002
args.push(escapeArg(filenames as string));
} }
// https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984 // https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984
args.push("</dev/null"); args.push("</dev/null");

View file

@ -66,7 +66,7 @@ export class AdbBanner {
return this.#device; return this.#device;
} }
readonly #features: AdbFeature[] = []; readonly #features: readonly AdbFeature[] = [];
get features() { get features() {
return this.#features; return this.#features;
} }
@ -75,7 +75,7 @@ export class AdbBanner {
product: string | undefined, product: string | undefined,
model: string | undefined, model: string | undefined,
device: string | undefined, device: string | undefined,
features: AdbFeature[], features: readonly AdbFeature[],
) { ) {
this.#product = product; this.#product = product;
this.#model = model; this.#model = model;

View file

@ -28,7 +28,9 @@ export class AdbNoneProtocolSubprocessService extends AdbNoneProtocolSpawner {
this.#adb = adb; this.#adb = adb;
} }
async pty(command?: string | string[]): Promise<AdbNoneProtocolPtyProcess> { async pty(
command?: string | readonly string[],
): Promise<AdbNoneProtocolPtyProcess> {
if (command === undefined) { if (command === undefined) {
command = ""; command = "";
} else if (Array.isArray(command)) { } else if (Array.isArray(command)) {
@ -36,7 +38,8 @@ export class AdbNoneProtocolSubprocessService extends AdbNoneProtocolSpawner {
} }
return new AdbNoneProtocolPtyProcess( return new AdbNoneProtocolPtyProcess(
await this.#adb.createSocket(`shell:${command}`), // https://github.com/microsoft/typescript/issues/17002
await this.#adb.createSocket(`shell:${command as string}`),
); );
} }
} }

View file

@ -28,13 +28,13 @@ export interface AdbNoneProtocolProcess {
export class AdbNoneProtocolSpawner { export class AdbNoneProtocolSpawner {
readonly #spawn: ( readonly #spawn: (
command: string[], command: readonly string[],
signal: AbortSignal | undefined, signal: AbortSignal | undefined,
) => Promise<AdbNoneProtocolProcess>; ) => Promise<AdbNoneProtocolProcess>;
constructor( constructor(
spawn: ( spawn: (
command: string[], command: readonly string[],
signal: AbortSignal | undefined, signal: AbortSignal | undefined,
) => Promise<AdbNoneProtocolProcess>, ) => Promise<AdbNoneProtocolProcess>,
) { ) {
@ -42,7 +42,7 @@ export class AdbNoneProtocolSpawner {
} }
spawn( spawn(
command: string | string[], command: string | readonly string[],
signal?: AbortSignal, signal?: AbortSignal,
): Promise<AdbNoneProtocolProcess> { ): Promise<AdbNoneProtocolProcess> {
signal?.throwIfAborted(); signal?.throwIfAborted();
@ -54,12 +54,12 @@ export class AdbNoneProtocolSpawner {
return this.#spawn(command, signal); return this.#spawn(command, signal);
} }
async spawnWait(command: string | string[]): Promise<Uint8Array> { async spawnWait(command: string | readonly string[]): Promise<Uint8Array> {
const process = await this.spawn(command); const process = await this.spawn(command);
return await process.output.pipeThrough(new ConcatBufferStream()); return await process.output.pipeThrough(new ConcatBufferStream());
} }
async spawnWaitText(command: string | string[]): Promise<string> { async spawnWaitText(command: string | readonly string[]): Promise<string> {
const process = await this.spawn(command); const process = await this.spawn(command);
return await process.output return await process.output
.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextDecoderStream())

View file

@ -32,7 +32,7 @@ export class AdbShellProtocolSubprocessService extends AdbShellProtocolSpawner {
} }
async pty(options?: { async pty(options?: {
command?: string | string[] | undefined; command?: string | readonly string[] | undefined;
terminalType?: string; terminalType?: string;
}): Promise<AdbShellProtocolPtyProcess> { }): Promise<AdbShellProtocolPtyProcess> {
let service = "shell,v2,pty"; let service = "shell,v2,pty";

View file

@ -26,13 +26,13 @@ export interface AdbShellProtocolProcess {
export class AdbShellProtocolSpawner { export class AdbShellProtocolSpawner {
readonly #spawn: ( readonly #spawn: (
command: string[], command: readonly string[],
signal: AbortSignal | undefined, signal: AbortSignal | undefined,
) => Promise<AdbShellProtocolProcess>; ) => Promise<AdbShellProtocolProcess>;
constructor( constructor(
spawn: ( spawn: (
command: string[], command: readonly string[],
signal: AbortSignal | undefined, signal: AbortSignal | undefined,
) => Promise<AdbShellProtocolProcess>, ) => Promise<AdbShellProtocolProcess>,
) { ) {
@ -40,7 +40,7 @@ export class AdbShellProtocolSpawner {
} }
spawn( spawn(
command: string | string[], command: string | readonly string[],
signal?: AbortSignal, signal?: AbortSignal,
): Promise<AdbShellProtocolProcess> { ): Promise<AdbShellProtocolProcess> {
signal?.throwIfAborted(); signal?.throwIfAborted();
@ -53,7 +53,7 @@ export class AdbShellProtocolSpawner {
} }
async spawnWait( async spawnWait(
command: string | string[], command: string | readonly string[],
): Promise<AdbShellProtocolSpawner.WaitResult<Uint8Array>> { ): Promise<AdbShellProtocolSpawner.WaitResult<Uint8Array>> {
const process = await this.spawn(command); const process = await this.spawn(command);
const [stdout, stderr, exitCode] = await Promise.all([ const [stdout, stderr, exitCode] = await Promise.all([
@ -65,7 +65,7 @@ export class AdbShellProtocolSpawner {
} }
async spawnWaitText( async spawnWaitText(
command: string | string[], command: string | readonly string[],
): Promise<AdbShellProtocolSpawner.WaitResult<string>> { ): Promise<AdbShellProtocolSpawner.WaitResult<string>> {
const process = await this.spawn(command); const process = await this.spawn(command);
const [stdout, stderr, exitCode] = await Promise.all([ const [stdout, stderr, exitCode] = await Promise.all([

View file

@ -139,7 +139,7 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
}; };
}; };
export const ADB_DEFAULT_AUTHENTICATORS: AdbAuthenticator[] = [ export const ADB_DEFAULT_AUTHENTICATORS: readonly AdbAuthenticator[] = [
AdbSignatureAuthenticator, AdbSignatureAuthenticator,
AdbPublicKeyAuthenticator, AdbPublicKeyAuthenticator,
]; ];

View file

@ -49,7 +49,7 @@ export const ADB_DAEMON_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
"sendrecv_v2_zstd", "sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send", "sendrecv_v2_dry_run_send",
AdbFeature.DelayedAck, AdbFeature.DelayedAck,
] as AdbFeature[])(); ] as readonly AdbFeature[])();
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024; export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
export type AdbDaemonConnection = ReadableWritablePair< export type AdbDaemonConnection = ReadableWritablePair<
@ -61,7 +61,7 @@ export interface AdbDaemonAuthenticationOptions {
serial: string; serial: string;
connection: AdbDaemonConnection; connection: AdbDaemonConnection;
credentialStore: AdbCredentialStore; credentialStore: AdbCredentialStore;
authenticators?: AdbAuthenticator[]; authenticators?: readonly AdbAuthenticator[];
features?: readonly AdbFeature[]; features?: readonly AdbFeature[];
/** /**

View file

@ -39,7 +39,7 @@ export class AdbServerClient {
static parseDeviceList( static parseDeviceList(
value: string, value: string,
includeStates: AdbServerClient.ConnectionState[] = [ includeStates: readonly AdbServerClient.ConnectionState[] = [
"device", "device",
"unauthorized", "unauthorized",
], ],
@ -201,7 +201,7 @@ export class AdbServerClient {
* Equivalent ADB Command: `adb devices -l` * Equivalent ADB Command: `adb devices -l`
*/ */
async getDevices( async getDevices(
includeStates: AdbServerClient.ConnectionState[] = [ includeStates: readonly AdbServerClient.ConnectionState[] = [
"device", "device",
"unauthorized", "unauthorized",
], ],
@ -249,7 +249,7 @@ export class AdbServerClient {
*/ */
async getDeviceFeatures( async getDeviceFeatures(
device: AdbServerClient.DeviceSelector, device: AdbServerClient.DeviceSelector,
): Promise<{ transportId: bigint; features: AdbFeature[] }> { ): Promise<{ transportId: bigint; features: readonly AdbFeature[] }> {
// On paper, `host:features` is a host service (device features are cached in host), // On paper, `host:features` is a host service (device features are cached in host),
// so it shouldn't use `createDeviceConnection`, // so it shouldn't use `createDeviceConnection`,
// which is used to forward the service to the device. // which is used to forward the service to the device.

View file

@ -14,7 +14,7 @@ export function unorderedRemove<T>(array: T[], index: number) {
} }
interface Observer { interface Observer {
includeStates: AdbServerClient.ConnectionState[]; includeStates: readonly AdbServerClient.ConnectionState[];
onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>; onDeviceAdd: EventEmitter<readonly AdbServerClient.Device[]>;
onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>; onDeviceRemove: EventEmitter<readonly AdbServerClient.Device[]>;
onListChange: EventEmitter<readonly AdbServerClient.Device[]>; onListChange: EventEmitter<readonly AdbServerClient.Device[]>;
@ -23,7 +23,7 @@ interface Observer {
function filterDeviceStates( function filterDeviceStates(
devices: readonly AdbServerClient.Device[], devices: readonly AdbServerClient.Device[],
states: AdbServerClient.ConnectionState[], states: readonly AdbServerClient.ConnectionState[],
) { ) {
return devices.filter((device) => states.includes(device.state)); return devices.filter((device) => states.includes(device.state));
} }
@ -214,6 +214,6 @@ export class AdbServerDeviceObserverOwner {
export namespace AdbServerDeviceObserverOwner { export namespace AdbServerDeviceObserverOwner {
export interface Options extends AdbServerClient.ServerConnectionOptions { export interface Options extends AdbServerClient.ServerConnectionOptions {
includeStates?: AdbServerClient.ConnectionState[]; includeStates?: readonly AdbServerClient.ConnectionState[];
} }
} }

View file

@ -30,7 +30,7 @@ export const ADB_SERVER_DEFAULT_FEATURES = /* #__PURE__ */ (() =>
"sendrecv_v2_lz4", "sendrecv_v2_lz4",
"sendrecv_v2_zstd", "sendrecv_v2_zstd",
"sendrecv_v2_dry_run_send", "sendrecv_v2_dry_run_send",
] as AdbFeature[])(); ] as readonly AdbFeature[])();
export class AdbServerTransport implements AdbTransport { export class AdbServerTransport implements AdbTransport {
#client: AdbServerClient; #client: AdbServerClient;

View file

@ -5,7 +5,7 @@ export interface AdbBackupOptions {
user: number; user: number;
saveSharedStorage?: boolean; saveSharedStorage?: boolean;
saveWidgets?: boolean; saveWidgets?: boolean;
packages: string[] | "user" | "all"; packages: readonly string[] | "user" | "all";
savePackageApk: boolean; savePackageApk: boolean;
savePackageObb: boolean; savePackageObb: boolean;
savePackageKeyValue: boolean; savePackageKeyValue: boolean;

View file

@ -53,8 +53,9 @@ export class CmdNoneProtocolService extends AdbNoneProtocolSpawner {
throw new Error("Unsupported"); throw new Error("Unsupported");
} }
command[0] = fallback; const fallbackCommand = command.slice();
return adb.subprocess.noneProtocol.spawn(command); fallbackCommand[0] = fallback;
return adb.subprocess.noneProtocol.spawn(fallbackCommand);
}); });
this.#supportsCmd = adb.canUseFeature(AdbFeature.Cmd); this.#supportsCmd = adb.canUseFeature(AdbFeature.Cmd);
@ -116,8 +117,9 @@ export class CmdShellProtocolService extends AdbShellProtocolSpawner {
throw new Error("Unsupported"); throw new Error("Unsupported");
} }
command[0] = fallback; const fallbackCommand = command.slice();
return adb.subprocess.shellProtocol.spawn(command); fallbackCommand[0] = fallback;
return adb.subprocess.shellProtocol.spawn(fallbackCommand);
}); });
this.#adb = adb; this.#adb = adb;

View file

@ -90,7 +90,7 @@ export interface LogcatFormatModifiers {
export interface LogcatOptions { export interface LogcatOptions {
dump?: boolean | undefined; dump?: boolean | undefined;
pid?: number | undefined; pid?: number | undefined;
ids?: LogId[] | undefined; ids?: readonly LogId[] | undefined;
tail?: number | Date | undefined; tail?: number | Date | undefined;
} }
@ -415,7 +415,7 @@ export class Logcat extends AdbServiceBase {
return LogId[key as keyof typeof LogId]; return LogId[key as keyof typeof LogId];
} }
static joinLogId(ids: LogId[]): string { static joinLogId(ids: readonly LogId[]): string {
return ids.map((id) => Logcat.logIdToName(id)).join(","); return ids.map((id) => Logcat.logIdToName(id)).join(",");
} }
@ -434,7 +434,7 @@ export class Logcat extends AdbServiceBase {
static readonly LOG_SIZE_REGEX_11: RegExp = static readonly LOG_SIZE_REGEX_11: RegExp =
/(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/; /(.*): ring buffer is (.*) (.*)B \((.*) (.*)B consumed, (.*) (.*)B readable\), max entry is (.*) B, max payload is (.*) B/;
async getLogSize(ids?: LogId[]): Promise<LogSize[]> { async getLogSize(ids?: readonly LogId[]): Promise<LogSize[]> {
const process = await this.adb.subprocess.noneProtocol.spawn([ const process = await this.adb.subprocess.noneProtocol.spawn([
"logcat", "logcat",
"-g", "-g",
@ -488,7 +488,7 @@ export class Logcat extends AdbServiceBase {
return result; return result;
} }
async clear(ids?: LogId[]): Promise<void> { async clear(ids?: readonly LogId[]): Promise<void> {
const args = ["logcat", "-c"]; const args = ["logcat", "-c"];
if (ids && ids.length > 0) { if (ids && ids.length > 0) {
args.push("-b", Logcat.joinLogId(ids)); args.push("-b", Logcat.joinLogId(ids));

View file

@ -81,7 +81,7 @@ export class OverlayDisplay extends AdbServiceBase {
})); }));
} }
async set(devices: OverlayDisplayDevice[]): Promise<void> { async set(devices: readonly OverlayDisplayDevice[]): Promise<void> {
await this.#settings.put( await this.#settings.put(
"global", "global",
OverlayDisplay.SETTING_KEY, OverlayDisplay.SETTING_KEY,

View file

@ -231,7 +231,7 @@ export interface PackageManagerUninstallOptions {
* *
* On Android 10 and lower, only one split name can be specified. * On Android 10 and lower, only one split name can be specified.
*/ */
splitNames: string[]; splitNames: readonly string[];
} }
const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record< const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record<
@ -298,7 +298,7 @@ export class PackageManager extends AdbServiceBase {
* @param apks Path to the apk file. It must exist on the device. On Android 10 and lower, only one apk can be specified. * @param apks Path to the apk file. It must exist on the device. On Android 10 and lower, only one apk can be specified.
*/ */
async install( async install(
apks: string[], apks: readonly string[],
options?: Partial<PackageManagerInstallOptions>, options?: Partial<PackageManagerInstallOptions>,
): Promise<string> { ): Promise<string> {
const args = buildInstallArguments("install", options); const args = buildInstallArguments("install", options);

View file

@ -1,11 +1,11 @@
export class ParseError extends Error { export class ParseError extends Error {
#expected: string[]; #expected: readonly string[];
get expected(): string[] { get expected(): readonly string[] {
return this.#expected; return this.#expected;
} }
constructor(expected: string[]) { constructor(expected: readonly string[]) {
super(`Expected ${expected.join(", ")}`); super(`Expected ${expected.join(", ")}`);
this.#expected = expected; this.#expected = expected;
} }

View file

@ -1,9 +1,9 @@
export function buildArguments<T>( export function buildArguments<T>(
commands: string[], commands: readonly string[],
options: Partial<T> | undefined, options: Partial<T> | undefined,
map: Partial<Record<keyof T, string>>, map: Partial<Record<keyof T, string>>,
): string[] { ): string[] {
const args = commands; const args = commands.slice();
if (options) { if (options) {
for (const [key, value] of Object.entries(options)) { for (const [key, value] of Object.entries(options)) {
if (value) { if (value) {

View file

@ -0,0 +1,22 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { Consumable } from "./consumable.js";
describe("Consumable", () => {
it("should export all symbols", () => {
assert(!!Consumable.WritableStream, "WritableStream should be define");
assert(
!!Consumable.WrapWritableStream,
"WrapWritableStream should be define",
);
assert(!!Consumable.ReadableStream, "ReadableStream should be define");
assert(
!!Consumable.WrapByteReadableStream,
"WrapByteReadableStream should be define",
);
});
});