@@ -39,6 +39,8 @@ import { fileURLToPath } from 'node:url';
3939
4040import { describe , expect , it } from 'vitest' ;
4141
42+ import { maskComments } from '../../../../scripts/js-comment-mask.mjs' ;
43+
4244const SPEC_ROOT = path . resolve ( path . dirname ( fileURLToPath ( import . meta. url ) ) , '../..' ) ;
4345const REPO_ROOT = path . resolve ( SPEC_ROOT , '../..' ) ;
4446const THIS_FILE = path . relative ( REPO_ROOT , fileURLToPath ( import . meta. url ) ) . split ( path . sep ) . join ( '/' ) ;
@@ -75,8 +77,13 @@ const PRODUCER_YAML = /^\s*(?:-\s+)?recipient_?[tT]ype\s*:\s*(['"]?)guest\1\s*(?
7577
7678const lineOf = ( text : string , index : number ) : number => text . slice ( 0 , index ) . split ( '\n' ) . length ;
7779
78- const producersIn = ( ext : string , text : string , live = 'guest' ) : number [ ] => {
79- if ( ! text . includes ( live ) ) return [ ] ;
80+ /** Extensions read as JS/TS source: their comments are blanked first, since a comment writes no row. */
81+ const CODE_EXT = new Set ( [ '.ts' , '.mts' , '.cts' , '.js' , '.mjs' , '.cjs' ] ) ;
82+
83+ const producersIn = ( ext : string , raw : string , live = 'guest' ) : number [ ] => {
84+ if ( ! raw . includes ( live ) ) return [ ] ;
85+ // `maskComments` blanks comment content and keeps offsets and line numbers.
86+ const text = CODE_EXT . has ( ext ) ? maskComments ( raw ) : raw ;
8087 const out : number [ ] = [ ] ;
8188 const code = new RegExp ( PRODUCER_CODE . source . replace ( 'guest' , live ) , 'g' ) ;
8289 for ( let m = code . exec ( text ) ; m ; m = code . exec ( text ) ) out . push ( lineOf ( text , m . index ) ) ;
@@ -87,7 +94,7 @@ const producersIn = (ext: string, text: string, live = 'guest'): number[] => {
8794 return [ ...new Set ( out ) ] . sort ( ( a , b ) => a - b ) ;
8895} ;
8996
90- /** The string literals inside the bracketed span that starts at `open`, comments stripped . */
97+ /** The string literals inside the bracketed span that starts at `open`, in comment-masked source . */
9198const literalsInBrackets = ( text : string , open : number ) : string [ ] => {
9299 let depth = 0 ;
93100 let end = - 1 ;
@@ -96,12 +103,12 @@ const literalsInBrackets = (text: string, open: number): string[] => {
96103 else if ( text [ i ] === ']' ) { depth -= 1 ; if ( depth === 0 ) { end = i ; break ; } }
97104 }
98105 if ( end < 0 ) return [ ] ;
99- const body = text . slice ( open + 1 , end ) . replace ( / \/ \/ [ ^ \n ] * / g, '' ) . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, '' ) ;
100- return [ ...body . matchAll ( / ( [ ' " ` ] ) ( [ ^ ' " ` ] * ) \1/ g) ] . map ( ( m ) => m [ 2 ] ! ) ;
106+ return [ ...text . slice ( open + 1 , end ) . matchAll ( / ( [ ' " ` ] ) ( [ ^ ' " ` ] * ) \1/ g) ] . map ( ( m ) => m [ 2 ] ! ) ;
101107} ;
102108
103109/** Leg 2: the options of the `recipient_type` select in a file that declares `sys_record_share`, or `null`. */
104- const recordShareSelectOptions = ( text : string ) : string [ ] | null => {
110+ const recordShareSelectOptions = ( raw : string ) : string [ ] | null => {
111+ const text = maskComments ( raw ) ;
105112 if ( ! / n a m e : \s * [ ' " ] s y s _ r e c o r d _ s h a r e [ ' " ] / . test ( text ) ) return null ;
106113 const at = text . search ( / r e c i p i e n t _ t y p e \s * : \s * F i e l d \. s e l e c t \( / ) ;
107114 if ( at < 0 ) return null ;
@@ -110,10 +117,10 @@ const recordShareSelectOptions = (text: string): string[] | null => {
110117} ;
111118
112119/** Leg 3: the literal members of `RecordShareRecipientType`, or `null` when the file does not declare it. */
113- const unionMembers = ( text : string ) : string [ ] | null => {
114- const m = / e x p o r t \s + t y p e \s + R e c o r d S h a r e R e c i p i e n t T y p e \s * = ( [ ^ ; ] * ) ; / . exec ( text ) ;
120+ const unionMembers = ( raw : string ) : string [ ] | null => {
121+ const m = / e x p o r t \s + t y p e \s + R e c o r d S h a r e R e c i p i e n t T y p e \s * = ( [ ^ ; ] * ) ; / . exec ( maskComments ( raw ) ) ;
115122 if ( ! m ) return null ;
116- return [ ...m [ 1 ] ! . replace ( / \/ \/ [ ^ \n ] * / g , '' ) . matchAll ( / ' ( [ ^ ' ] * ) ' / g) ] . map ( ( x ) => x [ 1 ] ! ) ;
123+ return [ ...m [ 1 ] ! . matchAll ( / ' ( [ ^ ' ] * ) ' / g) ] . map ( ( x ) => x [ 1 ] ! ) ;
117124} ;
118125
119126describe ( 'record-share guest recipient census — the matchers can see the value (planted positive controls)' , ( ) => {
@@ -130,6 +137,8 @@ describe('record-share guest recipient census — the matchers can see the value
130137 expect ( producersIn ( '.ts' , "({ anchor: 'guest', principalKind: 'guest', type: 'guest' })" ) ) . toEqual ( [ ] ) ;
131138 expect ( producersIn ( '.ts' , "({ recipient_type: 'guest_portal' })" ) ) . toEqual ( [ ] ) ;
132139 expect ( producersIn ( '.md' , 'The guest recipient type was removed.' ) ) . toEqual ( [ ] ) ;
140+ // A comment writes no row; the same words in a string literal do.
141+ expect ( producersIn ( '.ts' , "// once: recipient_type: 'guest'\nconst x = 1;" ) ) . toEqual ( [ ] ) ;
133142 } ) ;
134143
135144 it ( 'leg 2 reads the options of the record-share select, and finds a planted guest option' , ( ) => {
@@ -143,6 +152,8 @@ describe('record-share guest recipient census — the matchers can see the value
143152 it ( 'leg 3 reads the union members, and finds a planted guest member' , ( ) => {
144153 expect ( unionMembers ( "export type RecordShareRecipientType =\n | 'user'\n | 'guest';" ) ) . toEqual ( [ 'user' , 'guest' ] ) ;
145154 expect ( unionMembers ( "export type SharingRuleRecipientType = 'user' | 'guest';" ) ) . toBeNull ( ) ;
155+ // A member named only in a comment is not a member.
156+ expect ( unionMembers ( "export type RecordShareRecipientType =\n | 'user'\n // | 'guest'\n ;" ) ) . toEqual ( [ 'user' ] ) ;
146157 } ) ;
147158} ) ;
148159
0 commit comments