@@ -21,6 +21,64 @@ import {
2121import { oldestSupported } from '@/versions/lib/enterprise-server-releases'
2222import type { RuleParams , RuleErrorCallback } from '@/content-linter/types'
2323
24+ // A liquidjs token, as exposed by getLiquidIfVersionTokens. liquidjs's TopLevelToken
25+ // type does not declare all of the runtime properties we rely on (begin/end, content,
26+ // contentRange, name), so we narrow it here.
27+ type LiquidConditionalToken = TopLevelToken & {
28+ name : string
29+ content : string
30+ begin : number
31+ end : number
32+ contentRange : [ number , number ]
33+ }
34+
35+ // Frontmatter `versions` declaration. May be a wildcard string ("*") or a record
36+ // keyed by short version names (fpt, ghec, ghes, feature, ...) with semver-range values.
37+ type VersionsObject = Record < string , string >
38+ type FileVersionsFm = string | VersionsObject | undefined
39+
40+ type CondTagAction = {
41+ type : 'none' | 'delete' | 'all' | 'change'
42+ name ?: string
43+ cond ?: string
44+ line ?: unknown
45+ lineNumbers ?: unknown
46+ length ?: unknown
47+ column ?: unknown
48+ content ?: unknown
49+ }
50+
51+ // Internal representation of an ifversion/elsif/else/endif tag that flows through
52+ // the rule. fileVersionsFmAll, versionsObj, featureVersionsObj, versionsObjAll, and
53+ // versions are populated for ifversion/elsif tags and may be absent on else/endif.
54+ // `action` is always populated by decorateCondTagItems before setLiquidErrors and
55+ // updateConditionals run.
56+ type CondTagItem = {
57+ name : string
58+ cond : string
59+ begin : number
60+ end : number
61+ contentrange : [ number , number ]
62+ fileVersionsFm : FileVersionsFm
63+ fileVersionsFmAll : VersionsObject
64+ fileVersions : string [ ]
65+ parent ?: CondTagItem
66+ versionsObj : VersionsObject
67+ featureVersionsObj ?: VersionsObject
68+ versionsObjAll : VersionsObject
69+ versions : string [ ]
70+ action : CondTagAction
71+ // Cached error range (used by addError); never set by this rule but accepted by addError.
72+ contentRange ?: [ number , number ] | number [ ] | string | null
73+ }
74+
75+ type DefaultProps = {
76+ fileVersionsFm : FileVersionsFm
77+ fileVersions : string [ ]
78+ filename : string
79+ parent : CondTagItem | undefined
80+ }
81+
2482export const liquidIfversionVersions = {
2583 names : [ 'GHD022' , 'liquid-ifversion-versions' ] ,
2684 description :
@@ -33,31 +91,27 @@ export const liquidIfversionVersions = {
3391 const fm = getFrontmatter ( params . lines )
3492 const content = fm ? getFrontmatterLines ( params . lines ) . join ( '\n' ) : params . lines . join ( '\n' )
3593
36- const fileVersionsFm = params . name . startsWith ( 'data' )
94+ const fileVersionsFm : FileVersionsFm = params . name . startsWith ( 'data' )
3795 ? { ghec : '*' , ghes : '*' , fpt : '*' }
3896 : fm
39- ? ( fm . versions as string | Record < string , string > | undefined )
40- : ( getFrontmatter ( params . frontMatterLines ) ?. versions as
41- | string
42- | Record < string , string >
43- | undefined )
97+ ? ( fm . versions as FileVersionsFm )
98+ : ( getFrontmatter ( params . frontMatterLines ) ?. versions as FileVersionsFm )
4499 if ( ! fileVersionsFm ) return
45100 // This will only contain valid (non-deprecated) and future versions
46101 const fileVersions = getApplicableVersions ( fileVersionsFm , '' , {
47102 doNotThrow : true ,
48103 includeNextVersion : true ,
49104 } )
50105
51- const tokens = getLiquidIfVersionTokens ( content )
106+ const tokens = getLiquidIfVersionTokens ( content ) as LiquidConditionalToken [ ]
52107 // Array of arrays - each array entry is an array of items that
53108 // make up a full if/elsif/else/endif statement.
54109 // [ [ifversion, elsif, else, endif], [nested ifversion, elsif, else, endif] ]
55- // Using any[] because these are complex dynamic objects with properties added at runtime
56- const condStmtStack : any [ ] = [ ]
110+ const condStmtStack : CondTagItem [ ] [ ] = [ ]
57111
58112 // Tokens are in the order they are read in file, so we need to iterate
59113 // through and group full if/elsif/else/endif statements together.
60- const defaultProps = {
114+ const defaultProps : DefaultProps = {
61115 fileVersionsFm,
62116 fileVersions,
63117 filename : params . name ,
@@ -74,27 +128,25 @@ export const liquidIfversionVersions = {
74128 const condTagItem = await initTagObject ( token , defaultProps )
75129 condStmtStack . push ( [ condTagItem ] )
76130 } else if ( token . name === 'elsif' ) {
77- const condTagItems = condStmtStack . pop ( )
131+ const condTagItems = condStmtStack . pop ( ) !
78132 const condTagItem = await initTagObject ( token , defaultProps )
79133 condTagItems . push ( condTagItem )
80134 condStmtStack . push ( condTagItems )
81135 } else if ( token . name === 'else' ) {
82- const condTagItems = condStmtStack . pop ( )
136+ const condTagItems = condStmtStack . pop ( ) !
83137 const condTagItem = await initTagObject ( token , defaultProps )
84138 // The versions of an else tag are the set of file versions that are
85139 // not supported by the previous ifversion or elsif tags.
86140 const siblingVersions = condTagItems
87- // Using any because condTagItems contains dynamic objects from initTagObject
88- . filter ( ( item : any ) => item . name === 'ifversion' || item . name === 'elsif' )
89- . map ( ( item : any ) => item . versions )
141+ . filter ( ( item ) => item . name === 'ifversion' || item . name === 'elsif' )
142+ . map ( ( item ) => item . versions )
90143 . flat ( )
91- // Using any because versions property is added dynamically to condTagItem
92- ; ( condTagItem as any ) . versions = difference ( fileVersions , siblingVersions )
144+ condTagItem . versions = difference ( fileVersions , siblingVersions )
93145 condTagItems . push ( condTagItem )
94146 condStmtStack . push ( condTagItems )
95147 } else if ( token . name === 'endif' ) {
96148 defaultProps . parent = undefined
97- const condTagItems = condStmtStack . pop ( )
149+ const condTagItems = condStmtStack . pop ( ) !
98150 const condTagItem = await initTagObject ( token , defaultProps )
99151 condTagItems . push ( condTagItem )
100152 decorateCondTagItems ( condTagItems )
@@ -104,18 +156,21 @@ export const liquidIfversionVersions = {
104156 } ,
105157}
106158
107- // Using any[] because condTagItems contains dynamic objects with properties added at runtime
108- function setLiquidErrors ( condTagItems : any [ ] , onError : RuleErrorCallback , lines : string [ ] ) {
159+ function setLiquidErrors ( condTagItems : CondTagItem [ ] , onError : RuleErrorCallback , lines : string [ ] ) {
109160 for ( let i = 0 ; i < condTagItems . length ; i ++ ) {
110161 const item = condTagItems [ i ]
111162 const tagNameNoCond = item . name === 'endif' || item . name === 'else'
112163 const itemErrorName = tagNameNoCond ? item . name : `${ item . name } ${ item . cond } `
113164
114- if ( item . action . type === 'delete' ) {
165+ if ( item . action ? .type === 'delete' ) {
115166 // There is no next stack item, the endif tag is alway the
116167 // last in a conditional
117168 const nextStackItem = item . name === 'endif' ? condTagItems [ i ] . end : condTagItems [ i + 1 ] . begin
118- const deleteItems = getContentDeleteData ( condTagItems [ i ] , nextStackItem , lines )
169+ const deleteItems = getContentDeleteData (
170+ condTagItems [ i ] as unknown as TopLevelToken ,
171+ nextStackItem ,
172+ lines ,
173+ )
119174 for ( const deleteItem of deleteItems ) {
120175 addError (
121176 onError ,
@@ -133,7 +188,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
133188 }
134189 }
135190
136- if ( item . action . type === 'all' ) {
191+ if ( item . action ? .type === 'all' ) {
137192 // position is just the tag
138193 const { lineNumber, column, length } = getPositionData (
139194 {
@@ -158,7 +213,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
158213 )
159214 }
160215
161- if ( item . action . type === 'change' ) {
216+ if ( item . action ? .type === 'change' ) {
162217 // position is just the inside of tag
163218 const { lineNumber, column, length } = getPositionData (
164219 {
@@ -186,9 +241,8 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
186241 }
187242}
188243
189- async function getApplicableVersionFromLiquidTag ( conditionStr : string ) {
190- // Using Record<string, any> because version object keys are dynamic (fpt, ghec, ghes, feature, etc.)
191- const newConditionObject : Record < string , any > = { }
244+ async function getApplicableVersionFromLiquidTag ( conditionStr : string ) : Promise < VersionsObject > {
245+ const newConditionObject : VersionsObject = { }
192246 const condition = conditionStr . replace ( 'not ' , '' )
193247 const liquidTagVersions = condition . split ( ' or ' ) . map ( ( item ) => item . trim ( ) )
194248 for ( const ver of liquidTagVersions ) {
@@ -211,7 +265,7 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) {
211265 // All actual uses have matching versions (e.g., "ghes and ghes > 3.19").
212266 // If this edge case appears in the future, additional logic would be needed here.
213267 if ( ! ands . every ( ( and ) => and . startsWith ( firstAnd ) ) ) {
214- return [ ]
268+ return { }
215269 }
216270 const andValues = [ ]
217271 let andVersion = ''
@@ -235,41 +289,56 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) {
235289 doNotThrow : true ,
236290 includeNextVersion : true ,
237291 } )
238- return await convertVersionsToFrontmatter ( difference ( all , allApplicable ) )
292+ return ( await convertVersionsToFrontmatter ( difference ( all , allApplicable ) ) ) as VersionsObject
239293 }
240294 return newConditionObject
241295}
242296
243- // Using any for token and props because they come from markdownlint library without full type definitions
244- async function initTagObject ( token : any , props : any ) {
245- const condTagItem = {
297+ async function initTagObject (
298+ token : LiquidConditionalToken ,
299+ props : DefaultProps ,
300+ ) : Promise < CondTagItem > {
301+ const fileVersionsFm = props . fileVersionsFm
302+ // Normalize a wildcard string ('*') frontmatter `versions` value into the
303+ // canonical all-versions object so downstream consumers (Object.keys, ghes /
304+ // feature lookups) behave consistently. In practice no content file uses the
305+ // string form today, but handling it keeps the rule type-safe and future-proof.
306+ const fmObject : VersionsObject =
307+ typeof fileVersionsFm === 'string'
308+ ? { ghec : '*' , ghes : '*' , fpt : '*' }
309+ : ( ( fileVersionsFm || { } ) as VersionsObject )
310+ const featureFromFm = fmObject . feature
311+ const condTagItem : CondTagItem = {
246312 name : token . name ,
247313 cond : token . content . replace ( `${ token . name } ` , '' ) . trim ( ) ,
248314 begin : token . begin ,
249315 end : token . end ,
250316 contentrange : token . contentRange ,
251- fileVersionsFm : props . fileVersionsFm ,
252- fileVersionsFmAll : props . fileVersionsFm ?. feature
317+ fileVersionsFm,
318+ fileVersionsFmAll : featureFromFm
253319 ? {
254- ...props . fileVersionsFm . versions ,
255- ...getFeatureVersionsObject ( props . fileVersionsFm . feature ) ,
320+ ...( ( fmObject as unknown as { versions ?: VersionsObject } ) . versions || { } ) ,
321+ ...getFeatureVersionsObject ( featureFromFm ) ,
256322 }
257- : props . fileVersionsFm ,
323+ : fmObject ,
258324 fileVersions : props . fileVersions ,
259325 parent : props . parent ,
326+ versionsObj : { } ,
327+ featureVersionsObj : undefined ,
328+ versionsObjAll : { } ,
329+ versions : [ ] ,
330+ action : { type : 'none' } ,
260331 }
261332 if ( token . name === 'ifversion' || token . name === 'elsif' ) {
262- // Using any because these properties (versionsObj, featureVersionsObj, versionsObjAll, versions)
263- // are added dynamically to condTagItem and not part of its initial type definition
264- ; ( condTagItem as any ) . versionsObj = await getApplicableVersionFromLiquidTag ( condTagItem . cond )
265- ; ( condTagItem as any ) . featureVersionsObj = ( condTagItem as any ) . versionsObj . feature
266- ? getFeatureVersionsObject ( ( condTagItem as any ) . versionsObj . feature )
333+ condTagItem . versionsObj = await getApplicableVersionFromLiquidTag ( condTagItem . cond )
334+ condTagItem . featureVersionsObj = condTagItem . versionsObj . feature
335+ ? getFeatureVersionsObject ( condTagItem . versionsObj . feature )
267336 : undefined
268- ; ( condTagItem as any ) . versionsObjAll = {
269- ...( condTagItem as any ) . versionsObj ,
270- ...( condTagItem as any ) . featureVersionsObj ,
337+ condTagItem . versionsObjAll = {
338+ ...condTagItem . versionsObj ,
339+ ...condTagItem . featureVersionsObj ,
271340 }
272- ; ( condTagItem as any ) . versions = getApplicableVersions ( ( condTagItem as any ) . versionsObj , '' , {
341+ condTagItem . versions = getApplicableVersions ( condTagItem . versionsObj , '' , {
273342 doNotThrow : true ,
274343 includeNextVersion : true ,
275344 } )
@@ -286,8 +355,7 @@ async function initTagObject(token: any, props: any) {
286355 Then create flaws per stack item.
287356 newCond
288357 */
289- // Using any[] because condTagItems contains dynamic objects with action property added at runtime
290- function decorateCondTagItems ( condTagItems : any [ ] ) {
358+ function decorateCondTagItems ( condTagItems : CondTagItem [ ] ) {
291359 for ( const item of condTagItems ) {
292360 item . action = {
293361 type : 'none' ,
@@ -304,8 +372,7 @@ function decorateCondTagItems(condTagItems: any[]) {
304372 return
305373}
306374
307- // Using any[] because condTagItems contains dynamic objects with various properties added at runtime
308- function updateConditionals ( condTagItems : any [ ] ) {
375+ function updateConditionals ( condTagItems : CondTagItem [ ] ) {
309376 // iterate through the ifversion, elsif, and else
310377 // tags but NOT the endif tag. endif tags have
311378 // no versions associated with them and are handled
@@ -317,7 +384,14 @@ function updateConditionals(condTagItems: any[]) {
317384 // the liquid should always be removed regardless
318385 // of whether it's a feature version or a nested
319386 // condition.
320- if ( isAllVersions ( item . featureVersionsObj || item . versionObj ) ) {
387+ // NOTE: Original code referenced `item.versionObj` (no `s`), which was always
388+ // undefined; preserved as-is to avoid changing runtime behavior in this PR.
389+ if (
390+ isAllVersions (
391+ item . featureVersionsObj ||
392+ ( ( item as unknown as { versionObj ?: VersionsObject } ) . versionObj as VersionsObject ) ,
393+ )
394+ ) {
321395 processConditionals ( item , condTagItems , i )
322396 break
323397 }
@@ -349,7 +423,9 @@ function updateConditionals(condTagItems: any[]) {
349423 continue
350424 }
351425 }
352- if ( item . versionsObj ?. feature || item . fileVersionsFm ?. feature ) break
426+ const fileVersionsFmObject =
427+ item . fileVersionsFm && typeof item . fileVersionsFm === 'object' ? item . fileVersionsFm : { }
428+ if ( item . versionsObj ?. feature || fileVersionsFmObject . feature ) break
353429
354430 // When the parent of a nested condition is a feature
355431 // we don't want to assume that the feature versions
@@ -478,8 +554,11 @@ function updateConditionals(condTagItems: any[]) {
478554 }
479555}
480556
481- // Using any for item and any[] for condTagItems because they contain dynamic objects with action property
482- function processConditionals ( item : any , condTagItems : any [ ] , indexOfAllItem : number ) {
557+ function processConditionals (
558+ item : CondTagItem ,
559+ condTagItems : CondTagItem [ ] ,
560+ indexOfAllItem : number ,
561+ ) {
483562 item . action . type = 'all'
484563 // if any tag in a statement is 'all', the
485564 // remaining tags are obsolete.
0 commit comments