88
99import ts from 'typescript' ;
1010
11+ import { ErrorCode , FatalDiagnosticError } from '../../../diagnostics' ;
1112import { absoluteFrom } from '../../../file_system' ;
1213import { runInEachFileSystem } from '../../../file_system/testing' ;
1314import { ImportedSymbolsTracker } from '../../../imports' ;
14- import { ClassMember , TypeScriptReflectionHost } from '../../../reflection' ;
15+ import { ClassMember , ClassMemberAccessLevel , TypeScriptReflectionHost } from '../../../reflection' ;
16+ import { reflectClassMember } from '../../../reflection/src/typescript' ;
1517import { makeProgram } from '../../../testing' ;
18+ import { validateAccessOfInitializerApiMember } from '../src/initializer_function_access' ;
1619import { InitializerApiFunction , tryParseInitializerApi } from '../src/initializer_functions' ;
1720
1821
1922runInEachFileSystem ( ( ) => {
2023 const modelApi : InitializerApiFunction = {
2124 functionName : 'model' ,
2225 owningModule : '@angular/core' ,
26+ allowedAccessLevels : [ ClassMemberAccessLevel . PublicWritable ] ,
2327 } ;
2428
2529 describe ( 'initializer function detection' , ( ) => {
@@ -54,7 +58,11 @@ runInEachFileSystem(() => {
5458
5559 const result = tryParseInitializerApi (
5660 [
57- { functionName : 'input' , owningModule : '@angular/core' } ,
61+ {
62+ functionName : 'input' ,
63+ owningModule : '@angular/core' ,
64+ allowedAccessLevels : [ ClassMemberAccessLevel . PublicWritable ] ,
65+ } ,
5866 modelApi ,
5967 ] ,
6068 member . value ! , reflector , importTracker ) ;
@@ -80,14 +88,19 @@ runInEachFileSystem(() => {
8088 const result = tryParseInitializerApi (
8189 [
8290 modelApi ,
83- { functionName : 'outputFromObservable' , owningModule : '@angular/core/rxjs-interop' } ,
91+ {
92+ functionName : 'outputFromObservable' ,
93+ owningModule : '@angular/core/rxjs-interop' ,
94+ allowedAccessLevels : [ ClassMemberAccessLevel . PublicWritable ] ,
95+ } ,
8496 ] ,
8597 member . value ! , reflector , importTracker ) ;
8698
8799 expect ( result ) . toEqual ( {
88100 api : {
89101 functionName : 'outputFromObservable' ,
90102 owningModule : '@angular/core/rxjs-interop' ,
103+ allowedAccessLevels : [ ClassMemberAccessLevel . PublicWritable ] ,
91104 } ,
92105 isRequired : false ,
93106 call : jasmine . objectContaining ( { kind : ts . SyntaxKind . CallExpression } ) ,
@@ -286,40 +299,142 @@ runInEachFileSystem(() => {
286299 call : jasmine . objectContaining ( { kind : ts . SyntaxKind . CallExpression } ) ,
287300 } ) ;
288301 } ) ;
302+
303+ describe ( '`validateAccessOfInitializerApiMember`' , ( ) => {
304+ it ( 'should report errors if a private field is used, but not allowed' , ( ) => {
305+ const { member, reflector, importTracker} = setup ( `
306+ import {Directive, model} from '@angular/core';
307+
308+ @Directive()
309+ class Dir {
310+ private test = model(1);
311+ }
312+ ` ) ;
313+
314+ const result = tryParseInitializerApi ( [ modelApi ] , member . value ! , reflector , importTracker ) ;
315+
316+ expect ( result ) . not . toBeNull ( ) ;
317+ expect ( ( ) => validateAccessOfInitializerApiMember ( result ! , member ) )
318+ . toThrowMatching (
319+ err => err instanceof FatalDiagnosticError &&
320+ err . code === ErrorCode . INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY ) ;
321+ } ) ;
322+
323+ it ( 'should report errors if a protected field is used, but not allowed' , ( ) => {
324+ const { member, reflector, importTracker} = setup ( `
325+ import {Directive, model} from '@angular/core';
326+
327+ @Directive()
328+ class Dir {
329+ protected test = model(1);
330+ }
331+ ` ) ;
332+
333+ const result = tryParseInitializerApi ( [ modelApi ] , member . value ! , reflector , importTracker ) ;
334+
335+ expect ( result ) . not . toBeNull ( ) ;
336+ expect ( ( ) => validateAccessOfInitializerApiMember ( result ! , member ) )
337+ . toThrowMatching (
338+ err => err instanceof FatalDiagnosticError &&
339+ err . code === ErrorCode . INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY ) ;
340+ } ) ;
341+
342+ it ( 'should report errors if an ECMAScript private field is used, but not allowed' , ( ) => {
343+ const { member, reflector, importTracker} = setup ( `
344+ import {Directive, model} from '@angular/core';
345+
346+ @Directive()
347+ class Dir {
348+ #test = model(1);
349+ }
350+ ` ) ;
351+
352+ const result = tryParseInitializerApi ( [ modelApi ] , member . value ! , reflector , importTracker ) ;
353+
354+ expect ( result ) . not . toBeNull ( ) ;
355+ expect ( ( ) => validateAccessOfInitializerApiMember ( result ! , member ) )
356+ . toThrowMatching (
357+ err => err instanceof FatalDiagnosticError &&
358+ err . code === ErrorCode . INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY ) ;
359+ } ) ;
360+
361+ it ( 'should report errors if a readonly public field is used, but not allowed' , ( ) => {
362+ const { member, reflector, importTracker} = setup ( `
363+ import {Directive, model} from '@angular/core';
364+
365+ @Directive()
366+ class Dir {
367+ // test model initializer API definition doesn't even allow readonly!
368+ readonly test = model(1);
369+ }
370+ ` ) ;
371+
372+ const result = tryParseInitializerApi ( [ modelApi ] , member . value ! , reflector , importTracker ) ;
373+
374+ expect ( result ) . not . toBeNull ( ) ;
375+ expect ( ( ) => validateAccessOfInitializerApiMember ( result ! , member ) )
376+ . toThrowMatching (
377+ err => err instanceof FatalDiagnosticError &&
378+ err . code === ErrorCode . INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY ) ;
379+ } ) ;
380+
381+ it ( 'should allow private field if API explicitly allows it' , ( ) => {
382+ const { member, reflector, importTracker} = setup ( `
383+ import {Directive, model} from '@angular/core';
384+
385+ @Directive()
386+ class Dir {
387+ // test model initializer API definition doesn't even allow readonly!
388+ private test = model(1);
389+ }
390+ ` ) ;
391+
392+ const result = tryParseInitializerApi (
393+ [ { ...modelApi , allowedAccessLevels : [ ClassMemberAccessLevel . Private ] } ] , member . value ! ,
394+ reflector , importTracker ) ;
395+
396+ expect ( result ?. api ) . toEqual ( jasmine . objectContaining < InitializerApiFunction > ( {
397+ functionName : 'model'
398+ } ) ) ;
399+ expect ( ( ) => validateAccessOfInitializerApiMember ( result ! , member ) ) . not . toThrow ( ) ;
400+ } ) ;
401+ } ) ;
289402} ) ;
290403
291404
292405function setup ( contents : string ) {
293406 const fileName = absoluteFrom ( '/test.ts' ) ;
294- const { program} = makeProgram ( [
295- {
296- name : absoluteFrom ( '/node_modules/@angular/core/index.d.ts' ) ,
297- contents : `
407+ const { program} = makeProgram (
408+ [
409+ {
410+ name : absoluteFrom ( '/node_modules/@angular/core/index.d.ts' ) ,
411+ contents : `
298412 export const Directive: any;
299413 export const input: any;
300414 export const model: any;
301415 ` ,
302- } ,
303- {
304- name : absoluteFrom ( '/node_modules/@angular/core/rxjs-interop/index.d.ts' ) ,
305- contents : `
416+ } ,
417+ {
418+ name : absoluteFrom ( '/node_modules/@angular/core/rxjs-interop/index.d.ts' ) ,
419+ contents : `
306420 export const outputFromObservable: any;
307421 ` ,
308- } ,
309- {
310- name : absoluteFrom ( '/node_modules/@unknown/utils/index.d.ts' ) ,
311- contents : `
422+ } ,
423+ {
424+ name : absoluteFrom ( '/node_modules/@unknown/utils/index.d.ts' ) ,
425+ contents : `
312426 export declare function toString(value: any): string;
313427 ` ,
314- } ,
315- {
316- name : absoluteFrom ( '/node_modules/@not-angular/core/index.d.ts' ) ,
317- contents : `
428+ } ,
429+ {
430+ name : absoluteFrom ( '/node_modules/@not-angular/core/index.d.ts' ) ,
431+ contents : `
318432 export const model: any;
319433 ` ,
320- } ,
321- { name : fileName , contents}
322- ] ) ;
434+ } ,
435+ { name : fileName , contents}
436+ ] ,
437+ { target : ts . ScriptTarget . ESNext } ) ;
323438 const sourceFile = program . getSourceFile ( fileName ) ;
324439 const importTracker = new ImportedSymbolsTracker ( ) ;
325440 const reflector = new TypeScriptReflectionHost ( program . getTypeChecker ( ) ) ;
@@ -328,11 +443,13 @@ function setup(contents: string) {
328443 throw new Error ( `Cannot resolve test file ${ fileName } ` ) ;
329444 }
330445
331- let member : Pick < ClassMember , 'value' > | null = null ;
446+ let member : Pick < ClassMember , 'value' | 'accessLevel' > | null = null ;
332447
333448 ( function walk ( node : ts . Node ) {
334- if ( ts . isPropertyDeclaration ( node ) && ts . isIdentifier ( node . name ) && node . name . text === 'test' ) {
335- member = { value : node . initializer ?? null } ;
449+ if ( ts . isPropertyDeclaration ( node ) &&
450+ ( ts . isIdentifier ( node . name ) && node . name . text === 'test' ||
451+ ts . isPrivateIdentifier ( node . name ) && node . name . text === '#test' ) ) {
452+ member = reflectClassMember ( node ) ;
336453 } else {
337454 ts . forEachChild ( node , walk ) ;
338455 }
0 commit comments