@@ -46,7 +46,7 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
4646export const CONDITION_READS_ENVIRONMENT_KEY = '_readsEnvironmentVariables'
4747
4848/** The sandbox global holding the run's secrets, in whatever shape an expression reaches it. */
49- const ENVIRONMENT_MAP_IDENTIFIER = / \b e n v i r o n m e n t V a r i a b l e s \b /
49+ const ENVIRONMENT_MAP_IDENTIFIER = / \b e n v i r o n m e n t V a r i a b l e s \b / g
5050
5151/** Key used to carry pre-resolved context variables through the inputs map. */
5252export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars'
@@ -362,7 +362,7 @@ export class VariableResolver {
362362 // the environment map is indistinguishable from one that merely quotes trigger
363363 // data containing the word, and the handler decides what to mount from this.
364364 [ CONDITION_READS_ENVIRONMENT_KEY ] :
365- typeof value === 'string' && ENVIRONMENT_MAP_IDENTIFIER . test ( value ) ,
365+ typeof value === 'string' && this . readsEnvironmentMap ( value ) ,
366366 value :
367367 typeof value === 'string'
368368 ? await this . resolveTemplateWithoutConditionFormatting (
@@ -1181,6 +1181,28 @@ export class VariableResolver {
11811181 )
11821182 }
11831183
1184+ /**
1185+ * Whether a Condition expression reads the run's secrets off the environment map.
1186+ *
1187+ * The name is only a read where it can execute, so each occurrence is placed with the same
1188+ * scanner that decides how references are spliced — a mention inside a string, a template,
1189+ * or a regex is text and mounts nothing. No shape of the read itself is assumed:
1190+ * `environmentVariables?.FLAG` and `Object.keys(environmentVariables)` both count, because
1191+ * narrowing an expression that does reach the map would route the run down a branch the
1192+ * author did not write, silently, while admitting one too many only costs the narrowing.
1193+ */
1194+ private readsEnvironmentMap ( expression : string ) : boolean {
1195+ ENVIRONMENT_MAP_IDENTIFIER . lastIndex = 0
1196+ let match = ENVIRONMENT_MAP_IDENTIFIER . exec ( expression )
1197+ while ( match !== null ) {
1198+ if ( this . getCodeStringQuoteContext ( expression , match . index , 'javascript' ) === null ) {
1199+ return true
1200+ }
1201+ match = ENVIRONMENT_MAP_IDENTIFIER . exec ( expression )
1202+ }
1203+ return false
1204+ }
1205+
11841206 /**
11851207 * Whether the `(` at this index opens a control-flow head rather than a value.
11861208 *
@@ -1204,19 +1226,27 @@ export class VariableResolver {
12041226
12051227 // `p.catch(fn)` is a method call whose name happens to be a keyword, and what follows its
12061228 // `)` is an operator, not a statement. A control-flow head can never be a property access,
1207- // and a comment can hide the dot (`p./* c */catch(fn)`), so a comment ending here is read
1208- // as the method call it usually is: the wrong guess there costs a division scanned as a
1209- // regex, while this way it costs nothing a regex-free line would notice.
1210- let before = start
1211- while ( before > 0 && WHITESPACE_CHAR . test ( template [ before - 1 ] ) ) {
1212- before --
1213- }
1214- if ( template [ before - 1 ] === '/' && template [ before - 2 ] === '*' ) {
1215- return false
1229+ // and a comment can stand between the two (`p./* c */catch(fn)`), so a comment is stepped
1230+ // over rather than treated as an answer — `/* c */ if (x)` is still a head.
1231+ let before = this . skipWhitespaceBackward ( template , start )
1232+ while ( template [ before - 1 ] === '/' && template [ before - 2 ] === '*' ) {
1233+ const opening = template . lastIndexOf ( '/*' , before - 2 )
1234+ if ( opening < 0 ) {
1235+ return true
1236+ }
1237+ before = this . skipWhitespaceBackward ( template , opening )
12161238 }
12171239 return template [ before - 1 ] !== '.'
12181240 }
12191241
1242+ private skipWhitespaceBackward ( template : string , index : number ) : number {
1243+ let cursor = index
1244+ while ( cursor > 0 && WHITESPACE_CHAR . test ( template [ cursor - 1 ] ) ) {
1245+ cursor --
1246+ }
1247+ return cursor
1248+ }
1249+
12201250 private matchesKeywordAt ( template : string , index : number , keyword : string ) : boolean {
12211251 if ( ! template . startsWith ( keyword , index ) ) {
12221252 return false
0 commit comments