Skip to content

Commit 871ddef

Browse files
committed
feat(context): use Readonly to guard immutable values
The built-in Readonly<T> type from TypeScript allows us to guard readonly usage of objects. Please note Readonly does not make the object completely immutable, for example, binding.tags is a Set<string> and tags can still be added/removed even binding.tags itself is readonly.
1 parent 9b1e26c commit 871ddef

File tree

5 files changed

+35
-30
lines changed

5 files changed

+35
-30
lines changed

packages/context/src/context.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,20 @@ export class Context {
143143
* - `*` matches zero or more characters except `.` and `:`
144144
* - `?` matches exactly one character except `.` and `:`
145145
*/
146-
find(pattern?: string | RegExp): Binding[];
146+
find(pattern?: string | RegExp): Readonly<Binding>[];
147147

148148
/**
149149
* Find bindings using a filter function
150150
* @param filter A function to test on the binding. It returns `true` to
151151
* include the binding or `false` to exclude the binding.
152152
*/
153-
find(filter: (binding: Binding) => boolean): Binding[];
153+
find(filter: (binding: Readonly<Binding>) => boolean): Readonly<Binding>[];
154154

155-
find(pattern?: string | RegExp | ((binding: Binding) => boolean)): Binding[] {
156-
let bindings: Binding[] = [];
157-
let filter: (binding: Binding) => boolean;
155+
find(
156+
pattern?: string | RegExp | ((binding: Binding) => boolean),
157+
): Readonly<Binding>[] {
158+
let bindings: Readonly<Binding>[] = [];
159+
let filter: (binding: Readonly<Binding>) => boolean;
158160
if (!pattern) {
159161
filter = binding => true;
160162
} else if (typeof pattern === 'string') {
@@ -182,13 +184,16 @@ export class Context {
182184
* - `*` matches zero or more characters except `.` and `:`
183185
* - `?` matches exactly one character except `.` and `:`
184186
*/
185-
findByTag(pattern: string | RegExp): Binding[] {
187+
findByTag(pattern: string | RegExp): Readonly<Binding>[] {
186188
const regexp =
187189
typeof pattern === 'string' ? this.wildcardToRegExp(pattern) : pattern;
188190
return this.find(b => Array.from(b.tags).some(t => regexp.test(t)));
189191
}
190192

191-
protected _mergeWithParent(childList: Binding[], parentList?: Binding[]) {
193+
protected _mergeWithParent(
194+
childList: Readonly<Binding>[],
195+
parentList?: Readonly<Binding>[],
196+
) {
192197
if (!parentList) return childList;
193198
const additions = parentList.filter(parentBinding => {
194199
// children bindings take precedence

packages/context/src/inject.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const PROPERTIES_KEY = 'inject:properties';
2323
export interface ResolverFunction {
2424
(
2525
ctx: Context,
26-
injection: Injection,
26+
injection: Readonly<Injection>,
2727
session?: ResolutionSession,
2828
): ValueOrPromise<BoundValue>;
2929
}
@@ -250,7 +250,7 @@ export namespace inject {
250250

251251
function resolveAsGetter(
252252
ctx: Context,
253-
injection: Injection,
253+
injection: Readonly<Injection>,
254254
session?: ResolutionSession,
255255
) {
256256
// We need to clone the session for the getter as it will be resolved later
@@ -279,9 +279,9 @@ function resolveAsSetter(ctx: Context, injection: Injection) {
279279
export function describeInjectedArguments(
280280
target: Object,
281281
method?: string | symbol,
282-
): Injection[] {
282+
): Readonly<Injection>[] {
283283
method = method || '';
284-
const meta = MetadataInspector.getAllParameterMetadata<Injection>(
284+
const meta = MetadataInspector.getAllParameterMetadata<Readonly<Injection>>(
285285
PARAMETERS_KEY,
286286
target,
287287
method,
@@ -291,7 +291,7 @@ export function describeInjectedArguments(
291291

292292
function resolveByTag(
293293
ctx: Context,
294-
injection: Injection,
294+
injection: Readonly<Injection>,
295295
session?: ResolutionSession,
296296
) {
297297
const tag: string | RegExp = injection.metadata!.tag;
@@ -311,9 +311,9 @@ function resolveByTag(
311311
*/
312312
export function describeInjectedProperties(
313313
target: Object,
314-
): MetadataMap<Injection> {
314+
): MetadataMap<Readonly<Injection>> {
315315
const metadata =
316-
MetadataInspector.getAllPropertyMetadata<Injection>(
316+
MetadataInspector.getAllPropertyMetadata<Readonly<Injection>>(
317317
PROPERTIES_KEY,
318318
target,
319319
) || {};

packages/context/src/resolution-session.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export type ResolutionAction = (
2424
*/
2525
export interface BindingElement {
2626
type: 'binding';
27-
value: Binding;
27+
value: Readonly<Binding>;
2828
}
2929

3030
/**
3131
* Wrapper for injections tracked by resolution sessions
3232
*/
3333
export interface InjectionElement {
3434
type: 'injection';
35-
value: Injection;
35+
value: Readonly<Injection>;
3636
}
3737

3838
/**
@@ -90,7 +90,7 @@ export class ResolutionSession {
9090
* @param session The current resolution session
9191
*/
9292
private static enterBinding(
93-
binding: Binding,
93+
binding: Readonly<Binding>,
9494
session?: ResolutionSession,
9595
): ResolutionSession {
9696
session = session || new ResolutionSession();
@@ -106,7 +106,7 @@ export class ResolutionSession {
106106
*/
107107
static runWithBinding(
108108
action: ResolutionAction,
109-
binding: Binding,
109+
binding: Readonly<Binding>,
110110
session?: ResolutionSession,
111111
) {
112112
const resolutionSession = ResolutionSession.enterBinding(binding, session);
@@ -122,7 +122,7 @@ export class ResolutionSession {
122122
* @param session The current resolution session
123123
*/
124124
private static enterInjection(
125-
injection: Injection,
125+
injection: Readonly<Injection>,
126126
session?: ResolutionSession,
127127
): ResolutionSession {
128128
session = session || new ResolutionSession();
@@ -138,7 +138,7 @@ export class ResolutionSession {
138138
*/
139139
static runWithInjection(
140140
action: ResolutionAction,
141-
injection: Injection,
141+
injection: Readonly<Injection>,
142142
session?: ResolutionSession,
143143
) {
144144
const resolutionSession = ResolutionSession.enterInjection(
@@ -155,7 +155,7 @@ export class ResolutionSession {
155155
* Describe the injection for debugging purpose
156156
* @param injection Injection object
157157
*/
158-
static describeInjection(injection?: Injection) {
158+
static describeInjection(injection?: Readonly<Injection>) {
159159
/* istanbul ignore if */
160160
if (injection == null) return undefined;
161161
const name = getTargetName(
@@ -175,7 +175,7 @@ export class ResolutionSession {
175175
* Push the injection onto the session
176176
* @param injection Injection The current injection
177177
*/
178-
pushInjection(injection: Injection) {
178+
pushInjection(injection: Readonly<Injection>) {
179179
/* istanbul ignore if */
180180
if (debugSession.enabled) {
181181
debugSession(
@@ -214,7 +214,7 @@ export class ResolutionSession {
214214
/**
215215
* Getter for the current injection
216216
*/
217-
get currentInjection(): Injection | undefined {
217+
get currentInjection(): Readonly<Injection> | undefined {
218218
for (let i = this.stack.length - 1; i >= 0; i--) {
219219
const element = this.stack[i];
220220
if (isInjection(element)) return element.value;
@@ -225,7 +225,7 @@ export class ResolutionSession {
225225
/**
226226
* Getter for the current binding
227227
*/
228-
get currentBinding(): Binding | undefined {
228+
get currentBinding(): Readonly<Binding> | undefined {
229229
for (let i = this.stack.length - 1; i >= 0; i--) {
230230
const element = this.stack[i];
231231
if (isBinding(element)) return element.value;
@@ -237,7 +237,7 @@ export class ResolutionSession {
237237
* Enter the resolution of the given binding. If
238238
* @param binding Binding
239239
*/
240-
pushBinding(binding: Binding) {
240+
pushBinding(binding: Readonly<Binding>) {
241241
/* istanbul ignore if */
242242
if (debugSession.enabled) {
243243
debugSession('Enter binding:', binding.toJSON());
@@ -260,7 +260,7 @@ export class ResolutionSession {
260260
/**
261261
* Exit the resolution of a binding
262262
*/
263-
popBinding() {
263+
popBinding(): Readonly<Binding> {
264264
const top = this.stack.pop();
265265
if (!isBinding(top)) {
266266
throw new Error('The top element must be a binding');
@@ -277,14 +277,14 @@ export class ResolutionSession {
277277
/**
278278
* Getter for bindings on the stack
279279
*/
280-
get bindingStack(): Binding[] {
280+
get bindingStack(): Readonly<Binding>[] {
281281
return this.stack.filter(isBinding).map(e => e.value);
282282
}
283283

284284
/**
285285
* Getter for injections on the stack
286286
*/
287-
get injectionStack(): Injection[] {
287+
get injectionStack(): Readonly<Injection>[] {
288288
return this.stack.filter(isInjection).map(e => e.value);
289289
}
290290

packages/context/src/resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function instantiateClass<T>(
111111
*/
112112
function resolve<T>(
113113
ctx: Context,
114-
injection: Injection,
114+
injection: Readonly<Injection>,
115115
session?: ResolutionSession,
116116
): ValueOrPromise<T> {
117117
/* istanbul ignore if */

packages/context/test/unit/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ describe('Context', () => {
202202
});
203203

204204
it('escapes reserved chars for regexp', () => {
205-
const b1 = ctx.bind('foo');
205+
ctx.bind('foo');
206206
const b2 = ctx.bind('foo+bar');
207207
const b3 = ctx.bind('foo|baz');
208208
let result = ctx.find('fo+');

0 commit comments

Comments
 (0)