CODEBASE: Merge TypeAssertion files (#1922)

This commit is contained in:
catloversg
2025-01-25 02:06:39 +07:00
committed by GitHub
parent f0a0d3095e
commit 9bf408221c
19 changed files with 125 additions and 111 deletions

View File

@@ -51,7 +51,7 @@ import { Sleeve } from "../PersonObjects/Sleeve/Sleeve";
import { autoCompleteTypeShorthand } from "./utils/terminalShorthands";
import { resolveTeamCasualties, type OperationTeam } from "./Actions/TeamCasualties";
import { shuffleArray } from "../Infiltration/ui/BribeGame";
import { objectAssert } from "../utils/helpers/typeAssertion";
import { assertObject } from "../utils/TypeAssertion";
import { throwIfReachable } from "../utils/helpers/throwIfReachable";
import { loadActionIdentifier } from "./utils/loadActionIdentifier";
@@ -1424,7 +1424,7 @@ export class Bladeburner implements OperationTeam {
/** Initializes a Bladeburner object from a JSON save state. */
static fromJSON(value: IReviverValue): Bladeburner {
objectAssert(value.data);
assertObject(value.data);
// operations and contracts are not loaded directly from the save, we load them in using a different method
const contractsData = value.data.contracts;
const operationsData = value.data.operations;

View File

@@ -4,7 +4,7 @@ import { codingContractTypesMetadata } from "./data/codingcontracttypes";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "./utils/JSONReviver";
import { CodingContractEvent } from "./ui/React/CodingContractModal";
import { ContractFilePath, resolveContractFilePath } from "./Paths/ContractFilePath";
import { objectAssert } from "./utils/helpers/typeAssertion";
import { assertObject } from "./utils/TypeAssertion";
/* Contract Types */
export const CodingContractTypes = Object.fromEntries(codingContractTypesMetadata.map((x) => [x.name, x]));
@@ -128,7 +128,7 @@ export class CodingContract {
/** Initializes a CodingContract from a JSON save state. */
static fromJSON(value: IReviverValue): CodingContract {
objectAssert(value.data);
assertObject(value.data);
// In previous versions, there was a data field instead of a state field.
if ("data" in value.data) {
value.data.state = value.data.data;

View File

@@ -17,7 +17,13 @@ export function basicErrorMessage(ws: WorkerScript | ScriptDeath, msg: string, t
return `${type} ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n${msg}`;
}
/** Creates an error message string with a stack trace. */
/**
* Creates an error message string with a stack trace.
*
* When the player provides invalid input, we try to provide a stack trace that points to the player's invalid caller,
* but we don't have an error instance with a stack trace. In order to get that stack trace, we create a new error
* instance, then remove "unrelated" traces (code in our codebase) and leave only traces of the player's code.
*/
export function errorMessage(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
const errstack = new Error().stack;
if (errstack === undefined) throw new Error("how did we not throw an error?");

View File

@@ -56,7 +56,7 @@ import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
import { ServerConstants } from "../Server/data/Constants";
import { basicErrorMessage, errorMessage, log } from "./ErrorMessages";
import { assertString, debugType } from "./TypeAssertion";
import { assertStringWithNSContext, debugType } from "./TypeAssertion";
import {
canAccessBitNodeFeature,
getDefaultBitNodeOptions,
@@ -124,7 +124,7 @@ export interface CompleteHGWOptions {
/** Convert a provided value v for argument argName to string. If it wasn't originally a string or number, throw. */
function string(ctx: NetscriptContext, argName: string, v: unknown): string {
if (typeof v === "number") v = v + ""; // cast to string;
assertString(ctx, argName, v);
assertStringWithNSContext(ctx, argName, v);
return v;
}
@@ -642,7 +642,7 @@ function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask {
}
export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath {
assertString(ctx, argName, filename);
assertStringWithNSContext(ctx, argName, filename);
const path = resolveFilePath(filename, ctx.workerScript.name);
if (path) return path;
throw errorMessage(ctx, `Invalid ${argName}, was not a valid path: ${filename}`);

View File

@@ -1,4 +1,4 @@
import { WorkerScript } from "./WorkerScript";
import type { WorkerScript } from "./WorkerScript";
/**
* Script death marker.

View File

@@ -23,10 +23,18 @@ export const debugType = (v: unknown): string => {
return `Is of type '${typeof v}', value: ${userFriendlyString(v)}`;
};
export function assertString(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string {
/**
* This function should be used to assert strings provided by the player. It uses a specialized utility function that
* provides a stack trace pointing to the player's invalid caller.
*/
export function assertStringWithNSContext(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string {
if (typeof v !== "string") throw errorMessage(ctx, `${argName} expected to be a string. ${debugType(v)}`, "TYPE");
}
export function assertFunction(ctx: NetscriptContext, argName: string, v: unknown): asserts v is () => void {
export function assertFunctionWithNSContext(
ctx: NetscriptContext,
argName: string,
v: unknown,
): asserts v is () => void {
if (typeof v !== "function") throw errorMessage(ctx, `${argName} expected to be a function ${debugType(v)}`, "TYPE");
}

View File

@@ -94,7 +94,7 @@ import { InternalAPI, setRemovedFunctions, NSProxy } from "./Netscript/APIWrappe
import { INetscriptExtra } from "./NetscriptFunctions/Extra";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { getBitNodeMultipliers } from "./BitNode/BitNode";
import { assert, arrayAssert, stringAssert, objectAssert } from "./utils/helpers/typeAssertion";
import { assert, assertArray, assertString, assertObject } from "./utils/TypeAssertion";
import { escapeRegExp } from "lodash";
import numeral from "numeral";
import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort, nextPortWrite } from "./NetscriptPort";
@@ -107,7 +107,7 @@ import { getRamCost } from "./Netscript/RamCostGenerator";
import { getEnumHelper } from "./utils/EnumHelper";
import { setDeprecatedProperties, deprecationWarning } from "./utils/DeprecationHelper";
import { ServerConstants } from "./Server/data/Constants";
import { assertFunction } from "./Netscript/TypeAssertion";
import { assertFunctionWithNSContext } from "./Netscript/TypeAssertion";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import { canAccessBitNodeFeature, validBitNodes } from "./BitNode/BitNodeUtils";
@@ -1694,11 +1694,11 @@ export const ns: InternalAPI<NSFull> = {
const options: { type?: string; choices?: string[] } = {};
_options ??= options;
const txt = helpers.string(ctx, "txt", _txt);
assert(_options, objectAssert, (type) =>
assert(_options, assertObject, (type) =>
helpers.errorMessage(ctx, `Invalid type for options: ${type}. Should be object.`, "TYPE"),
);
if (_options.type !== undefined) {
assert(_options.type, stringAssert, (type) =>
assert(_options.type, assertString, (type) =>
helpers.errorMessage(ctx, `Invalid type for options.type: ${type}. Should be string.`, "TYPE"),
);
options.type = _options.type;
@@ -1710,7 +1710,7 @@ export const ns: InternalAPI<NSFull> = {
);
}
if (options.type === "select") {
assert(_options.choices, arrayAssert, (type) =>
assert(_options.choices, assertArray, (type) =>
helpers.errorMessage(
ctx,
`Invalid type for options.choices: ${type}. If options.type is "select", options.choices must be an array.`,
@@ -1811,7 +1811,7 @@ export const ns: InternalAPI<NSFull> = {
}),
atExit: (ctx) => (callback, _id) => {
const id = _id ? helpers.string(ctx, "id", _id) : "default";
assertFunction(ctx, "callback", callback);
assertFunctionWithNSContext(ctx, "callback", callback);
ctx.workerScript.atExit.set(id, callback);
},
mv: (ctx) => (_host, _source, _destination) => {

View File

@@ -15,7 +15,7 @@ import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper";
import { Skills } from "../Bladeburner/data/Skills";
import { assertString } from "../Netscript/TypeAssertion";
import { assertStringWithNSContext } from "../Netscript/TypeAssertion";
import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations";
import { checkSleeveAPIAccess, checkSleeveNumber } from "../NetscriptFunctions/Sleeve";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
@@ -37,8 +37,8 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
};
function getAction(ctx: NetscriptContext, type: unknown, name: unknown): Action {
const bladeburner = Player.bladeburner;
assertString(ctx, "type", type);
assertString(ctx, "name", name);
assertStringWithNSContext(ctx, "type", type);
assertStringWithNSContext(ctx, "name", name);
if (bladeburner === null) throw new Error("Must have joined bladeburner");
const action = bladeburner.getActionFromTypeAndName(type, name);
if (!action) throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);

View File

@@ -10,7 +10,7 @@ import { scaleWorkStats } from "../../../Work/WorkStats";
import { getKeyList } from "../../../utils/helpers/getKeyList";
import { loadActionIdentifier } from "../../../Bladeburner/utils/loadActionIdentifier";
import { invalidWork } from "../../../Work/InvalidWork";
import { objectAssert } from "../../../utils/helpers/typeAssertion";
import { assertObject } from "../../../utils/TypeAssertion";
interface SleeveBladeburnerWorkParams {
actionId: ActionIdentifier & { type: BladeburnerActionType.General | BladeburnerActionType.Contract };
@@ -99,7 +99,7 @@ export class SleeveBladeburnerWork extends SleeveWorkClass {
/** Initializes a BladeburnerWork object from a JSON save state. */
static fromJSON(value: IReviverValue): SleeveBladeburnerWork {
objectAssert(value.data);
assertObject(value.data);
let actionId = loadActionIdentifier(value.data?.actionId);
if (!actionId) {
/**

View File

@@ -7,7 +7,7 @@ import { Sleeve } from "../Sleeve";
import { scaleWorkStats, WorkStats } from "../../../Work/WorkStats";
import { Locations } from "../../../Locations/Locations";
import { isMember } from "../../../utils/EnumHelper";
import { objectAssert } from "../../../utils/helpers/typeAssertion";
import { assertObject } from "../../../utils/TypeAssertion";
export const isSleeveClassWork = (w: SleeveWorkClass | null): w is SleeveClassWork =>
w !== null && w.type === SleeveWorkType.CLASS;
@@ -55,7 +55,7 @@ export class SleeveClassWork extends SleeveWorkClass {
/** Initializes a ClassWork object from a JSON save state. */
static fromJSON(value: IReviverValue): SleeveClassWork {
objectAssert(value.data);
assertObject(value.data);
if (typeof value.data.classType !== "string" || !(value.data.classType in Classes)) {
value.data.classType = "Computer Science";
}

View File

@@ -26,7 +26,7 @@ import { SaveDataError, canUseBinaryFormat, decodeSaveData, encodeJsonSaveString
import { isBinaryFormat } from "../electron/saveDataBinaryFormat";
import { downloadContentAsFile } from "./utils/FileUtils";
import { handleGetSaveDataInfoError } from "./utils/ErrorHandler";
import { isObject, objectAssert } from "./utils/helpers/typeAssertion";
import { isObject, assertObject } from "./utils/TypeAssertion";
import { evaluateVersionCompatibility } from "./utils/SaveDataMigrationUtils";
import { Reviver } from "./utils/GenericReviver";
@@ -99,7 +99,7 @@ export type BitburnerSaveObjectType = {
* If saveObject has these properties, we check if their values are strings.
*/
function assertBitburnerSaveObjectType(saveObject: unknown): asserts saveObject is BitburnerSaveObjectType {
objectAssert(saveObject);
assertObject(saveObject);
const mandatoryKeysOfSaveObj = [
"PlayerSave",

View File

@@ -13,7 +13,7 @@ import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { IPAddress, isIPAddress } from "../Types/strings";
import "../Script/RunningScript"; // For reviver side-effect
import { objectAssert } from "../utils/helpers/typeAssertion";
import { assertObject } from "../utils/TypeAssertion";
/**
* Map of all Servers that exist in the game
@@ -218,7 +218,7 @@ export function prestigeAllServers(): void {
export function loadAllServers(saveString: string): void {
const allServersData: unknown = JSON.parse(saveString, Reviver);
objectAssert(allServersData);
assertObject(allServersData);
for (const [serverName, server] of Object.entries(allServersData)) {
if (!(server instanceof Server) && !(server instanceof HacknetServer)) {
throw new Error(`Server ${serverName} is not an instance of Server or HacknetServer.`);

View File

@@ -24,7 +24,7 @@ import lodash from "lodash";
import { Settings } from "../Settings/Settings";
import type { ScriptKey } from "../utils/helpers/scriptKey";
import { objectAssert } from "../utils/helpers/typeAssertion";
import { assertObject } from "../utils/TypeAssertion";
import { clampNumber } from "../utils/helpers/clampNumber";
interface IConstructorParams {
@@ -295,7 +295,7 @@ export abstract class BaseServer implements IServer {
// RunningScripts are stored as a simple array, both for backward compatibility,
// compactness, and ease of filtering them here.
const result = Generic_toJSON(ctorName, this, keys);
objectAssert(result.data);
assertObject(result.data);
if (Settings.ExcludeRunningScriptsFromSave) {
result.data.runningScripts = [];
return result;
@@ -316,7 +316,7 @@ export abstract class BaseServer implements IServer {
// Initializes a Server Object from a JSON save state
// Called by subclasses, not Reviver.
static fromJSONBase<T extends BaseServer>(value: IReviverValue, ctor: new () => T, keys: readonly (keyof T)[]): T {
objectAssert(value.data);
assertObject(value.data);
const server = Generic_fromJSON(ctor, value.data, keys);
if (value.data.runningScripts != null && Array.isArray(value.data.runningScripts)) {
server.savedScripts = value.data.runningScripts;

View File

@@ -3,7 +3,7 @@ import { defaultTheme } from "../Themes/Themes";
import { defaultStyles } from "../Themes/Styles";
import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options";
import { defaultMonacoTheme } from "../ScriptEditor/ui/themes";
import { objectAssert } from "../utils/helpers/typeAssertion";
import { assertObject } from "../utils/TypeAssertion";
import { Result } from "../types";
import {
assertAndSanitizeEditorTheme,
@@ -186,7 +186,7 @@ export const Settings = {
load(saveString: string) {
const save: unknown = JSON.parse(saveString);
objectAssert(save);
assertObject(save);
save.overview && Object.assign(Settings.overview, save.overview);
try {
// Sanitize theme data. Invalid theme data may crash the game or make it stuck in the loading page.

View File

@@ -1,4 +1,4 @@
import { arrayAssert } from "../utils/helpers/typeAssertion";
import { assertArray } from "../utils/TypeAssertion";
import type { IReviverValue } from "../utils/JSONReviver";
// Versions of js builtin classes that can be converted to and from JSON for use in save files
@@ -7,7 +7,7 @@ export class JSONSet<T> extends Set<T> {
return { ctor: "JSONSet", data: Array.from(this) };
}
static fromJSON(value: IReviverValue): JSONSet<any> {
arrayAssert(value.data);
assertArray(value.data);
return new JSONSet(value.data);
}
}
@@ -18,9 +18,9 @@ export class JSONMap<K, __V> extends Map<K, __V> {
}
static fromJSON(value: IReviverValue): JSONMap<any, any> {
arrayAssert(value.data);
assertArray(value.data);
for (const item of value.data) {
arrayAssert(item);
assertArray(item);
if (item.length !== 2) {
console.error("Invalid data passed to JSONMap.fromJSON(). Value:", value);
throw new Error(`An item is not an array with exactly 2 items. Its length is ${item.length}.`);

View File

@@ -2,7 +2,7 @@ import type { Member } from "../types";
import type { NetscriptContext } from "../Netscript/APIWrapper";
import * as allEnums from "../Enums";
import { assertString } from "../Netscript/TypeAssertion";
import { assertStringWithNSContext } from "../Netscript/TypeAssertion";
import { errorMessage } from "../Netscript/ErrorMessages";
import { getRandomIntInclusive } from "./helpers/getRandomIntInclusive";
import { getRecordValues } from "../Types/Record";
@@ -43,7 +43,7 @@ class EnumHelper<EnumObj extends object, EnumMember extends Member<EnumObj> & st
if (match) return match;
// No match found, create error message
assertString(ctx, argName, toValidate);
assertStringWithNSContext(ctx, argName, toValidate);
let allowableValues = `Allowable values: ${this.valueArray.map((val) => `"${val}"`).join(", ")}`;
// Don't display all possibilities for large enums
if (this.valueArray.length > 10) {

View File

@@ -1,7 +1,7 @@
/* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */
import { ObjectValidator } from "./Validator";
import { JSONMap, JSONSet } from "../Types/Jsonable";
import { objectAssert } from "./helpers/typeAssertion";
import { assertObject } from "./TypeAssertion";
type JsonableClass = (new () => { toJSON: () => IReviverValue }) & {
fromJSON: (value: IReviverValue) => unknown;
@@ -58,7 +58,7 @@ export function Generic_fromJSON<T extends Record<string, any>>(
data: unknown,
keys?: readonly (keyof T)[],
): T {
objectAssert(data);
assertObject(data);
const obj = new ctor();
// If keys were provided, just load the provided keys (if they are in the data)
if (keys) {

View File

@@ -1,4 +1,74 @@
import type { Unknownify } from "../types";
// This function is empty because Unknownify<T> is a typesafe assertion on any object with no runtime checks needed.
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function assertLoadingType<T extends object>(val: object): asserts val is Unknownify<T> {}
export class TypeAssertionError extends Error {
friendlyType: string;
constructor(message: string, friendlyType: string, options?: ErrorOptions) {
super(message, options);
this.name = this.constructor.name;
this.friendlyType = friendlyType;
}
}
/** Function for providing custom error message to throw for a type assertion.
* @param v: Value to assert type of
* @param assertFn: Typechecking function to use for asserting type of v.
* @param msgFn: Function to use to generate an error message if an error is produced. */
export function assert<T>(
v: unknown,
assertFn: (v: unknown) => asserts v is T,
msgFn: (type: string) => string,
): asserts v is T {
try {
assertFn(v);
} catch (e) {
if (e instanceof TypeAssertionError) {
throw msgFn(e.friendlyType);
}
const type = typeof e === "string" ? e : "unknown";
throw msgFn(type);
}
}
/** Returns the friendlyType of v. arrays are "array" and null is "null". */
function getFriendlyType(v: unknown): string {
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
}
export function isObject(v: unknown): v is Record<string, unknown> {
return getFriendlyType(v) === "object";
}
/** For non-objects, and for array/null, throws an error with the friendlyType of v. */
export function assertObject(v: unknown): asserts v is Record<string, unknown> {
const type = getFriendlyType(v);
if (type !== "object") {
console.error("The value is not an object. Value:", v);
throw new TypeAssertionError(
`The value is not an object. Its type is ${type}. Its string value is ${String(v)}.`,
type,
);
}
}
/** For non-string, throws an error with the friendlyType of v. */
export function assertString(v: unknown): asserts v is string {
const type = getFriendlyType(v);
if (type !== "string") {
console.error("The value is not a string. Value:", v);
throw new TypeAssertionError(`The value is not an string. Its type is ${type}.`, type);
}
}
/** For non-array, throws an error with the friendlyType of v. */
export function assertArray(v: unknown): asserts v is unknown[] {
if (!Array.isArray(v)) {
console.error("The value is not an array. Value:", v);
const type = getFriendlyType(v);
throw new TypeAssertionError(`The value is not an array. Its type is ${type}.`, type);
}
}

View File

@@ -1,70 +0,0 @@
// Various functions for asserting types.
export class TypeAssertionError extends Error {
friendlyType: string;
constructor(message: string, friendlyType: string, options?: ErrorOptions) {
super(message, options);
this.name = this.constructor.name;
this.friendlyType = friendlyType;
}
}
/** Function for providing custom error message to throw for a type assertion.
* @param v: Value to assert type of
* @param assertFn: Typechecking function to use for asserting type of v.
* @param msgFn: Function to use to generate an error message if an error is produced. */
export function assert<T>(
v: unknown,
assertFn: (v: unknown) => asserts v is T,
msgFn: (type: string) => string,
): asserts v is T {
try {
assertFn(v);
} catch (e) {
if (e instanceof TypeAssertionError) {
throw msgFn(e.friendlyType);
}
const type = typeof e === "string" ? e : "unknown";
throw msgFn(type);
}
}
/** Returns the friendlyType of v. arrays are "array" and null is "null". */
function getFriendlyType(v: unknown): string {
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
}
export function isObject(v: unknown): v is Record<string, unknown> {
return getFriendlyType(v) === "object";
}
/** For non-objects, and for array/null, throws an error with the friendlyType of v. */
export function objectAssert(v: unknown): asserts v is Record<string, unknown> {
const type = getFriendlyType(v);
if (type !== "object") {
console.error("The value is not an object. Value:", v);
throw new TypeAssertionError(
`The value is not an object. Its type is ${type}. Its string value is ${String(v)}.`,
type,
);
}
}
/** For non-string, throws an error with the friendlyType of v. */
export function stringAssert(v: unknown): asserts v is string {
const type = getFriendlyType(v);
if (type !== "string") {
console.error("The value is not a string. Value:", v);
throw new TypeAssertionError(`The value is not an string. Its type is ${type}.`, type);
}
}
/** For non-array, throws an error with the friendlyType of v. */
export function arrayAssert(v: unknown): asserts v is unknown[] {
if (!Array.isArray(v)) {
console.error("The value is not an array. Value:", v);
const type = getFriendlyType(v);
throw new TypeAssertionError(`The value is not an array. Its type is ${type}.`, type);
}
}