11import { readFileSync , existsSync } from "node:fs" ;
22import { resolve } from "node:path" ;
33import * as core from "@actions/core" ;
4+ import yaml from "js-yaml" ;
45import { Severity } from "../analysis/types.js" ;
56import type { CommentMode } from "../analysis/types.js" ;
67import { DEFAULT_CONFIG } from "./defaults.js" ;
@@ -10,198 +11,13 @@ import type {
1011} from "./schema.js" ;
1112
1213/**
13- * Minimal YAML parser — handles the flat/nested key-value structures we need
14- * without pulling in a full YAML library. Supports:
15- * - Scalars (strings, numbers, booleans)
16- * - Nested objects via indentation
17- * - Arrays with `- item` syntax
18- * - Comments with `#`
19- *
20- * For production use this should be replaced with `yaml` or `js-yaml`, but
21- * keeping zero runtime deps for the action is preferable.
14+ * Parse YAML text into a plain object using js-yaml.
2215 */
2316function parseYAML ( text : string ) : Record < string , unknown > {
24- const rawLines = text . split ( "\n" ) ;
25- const root : Record < string , unknown > = { } ;
26-
27- // Stack tracks nested objects: { indent, obj, key (if this obj is a value under a key) }
28- const stack : Array < {
29- indent : number ;
30- obj : Record < string , unknown > ;
31- } > = [ { indent : - 2 , obj : root } ] ;
32-
33- // Track current array context: which parent obj, which key, and at what indent
34- let arrayCtx : {
35- parent : Record < string , unknown > ;
36- key : string ;
37- indent : number ;
38- } | null = null ;
39-
40- for ( const rawLine of rawLines ) {
41- // Strip inline comments (not inside quotes — good enough for config)
42- let line = rawLine ;
43- if ( ! line . trimStart ( ) . startsWith ( "#" ) ) {
44- const commentIdx = line . indexOf ( " #" ) ;
45- if ( commentIdx >= 0 ) {
46- line = line . slice ( 0 , commentIdx ) ;
47- }
48- }
49-
50- // Skip blank lines and full-line comments
51- if ( line . trim ( ) === "" || line . trimStart ( ) . startsWith ( "#" ) ) continue ;
52-
53- const indent = line . length - line . trimStart ( ) . length ;
54- const trimmed = line . trimStart ( ) ;
55-
56- // If indent drops below current array context, clear it
57- if ( arrayCtx && indent < arrayCtx . indent ) {
58- arrayCtx = null ;
59- }
60-
61- // ── Array item: "- value" or "- key: value, key: value" ──
62- if ( trimmed . startsWith ( "- " ) ) {
63- const itemContent = trimmed . slice ( 2 ) . trim ( ) ;
64-
65- // If we have no array context yet, we need to create one.
66- // The array should be the value of the last key set on the parent at
67- // the indentation level just above this one.
68- if ( ! arrayCtx || indent !== arrayCtx . indent ) {
69- // Pop stack so the top is the correct parent for this indent
70- while ( stack . length > 1 && stack [ stack . length - 1 ] . indent >= indent ) {
71- stack . pop ( ) ;
72- }
73-
74- // Strategy: look at the current top of stack. If it's an empty object
75- // placeholder (created by "key:" with no value), then the PARENT of
76- // this stack entry owns the key. Pop one more and convert that key's
77- // value from {} to [].
78- let found = false ;
79- const topEntry = stack [ stack . length - 1 ] ;
80-
81- if (
82- stack . length > 1 &&
83- Object . keys ( topEntry . obj ) . length === 0
84- ) {
85- // This empty object is the value of some key in the parent
86- const parentObj = stack [ stack . length - 2 ] . obj ;
87- const keys = Object . keys ( parentObj ) ;
88- for ( let k = keys . length - 1 ; k >= 0 ; k -- ) {
89- if ( parentObj [ keys [ k ] ] === topEntry . obj ) {
90- parentObj [ keys [ k ] ] = [ ] ;
91- // Pop the empty obj from the stack since it's now an array
92- stack . pop ( ) ;
93- arrayCtx = { parent : parentObj , key : keys [ k ] , indent } ;
94- found = true ;
95- break ;
96- }
97- }
98- }
99-
100- if ( ! found ) {
101- // Look for an empty object placeholder among the top's own keys
102- const parentObj = topEntry . obj ;
103- const keys = Object . keys ( parentObj ) ;
104- for ( let k = keys . length - 1 ; k >= 0 ; k -- ) {
105- const val = parentObj [ keys [ k ] ] ;
106- if (
107- typeof val === "object" &&
108- val !== null &&
109- ! Array . isArray ( val ) &&
110- Object . keys ( val ) . length === 0
111- ) {
112- parentObj [ keys [ k ] ] = [ ] ;
113- arrayCtx = { parent : parentObj , key : keys [ k ] , indent } ;
114- found = true ;
115- break ;
116- }
117- }
118- }
119-
120- if ( ! found ) {
121- // Cannot determine which key this array belongs to — skip
122- continue ;
123- }
124- }
125-
126- const arr = arrayCtx ! . parent [ arrayCtx ! . key ] as unknown [ ] ;
127-
128- // Check if it's a map item (has colon with a key-like prefix)
129- const mapMatch = itemContent . match (
130- / ^ ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) \s * : \s * ( .* ) $ / ,
131- ) ;
132- if ( mapMatch ) {
133- const mapObj : Record < string , unknown > = { } ;
134- parseInlineMap ( itemContent , mapObj ) ;
135- arr . push ( mapObj ) ;
136- } else {
137- arr . push ( parseScalar ( itemContent ) ) ;
138- }
139- continue ;
140- }
141-
142- // ── Key-value pair: "key: value" or "key:" ──
143- const kvMatch = trimmed . match ( / ^ ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) \s * : \s * ( .* ) $ / ) ;
144- if ( kvMatch ) {
145- const key = kvMatch [ 1 ] ;
146- const rawValue = kvMatch [ 2 ] . trim ( ) ;
147-
148- // Clear array context when we encounter a non-array line
149- arrayCtx = null ;
150-
151- // Pop stack to find the right parent for this indent level
152- while ( stack . length > 1 && stack [ stack . length - 1 ] . indent >= indent ) {
153- stack . pop ( ) ;
154- }
155- const parent = stack [ stack . length - 1 ] . obj ;
156-
157- if ( rawValue === "" || rawValue === "|" || rawValue === ">" ) {
158- // Nested object or block scalar — create object placeholder
159- // (may be converted to array if "- " items follow)
160- const child : Record < string , unknown > = { } ;
161- parent [ key ] = child ;
162- stack . push ( { indent, obj : child } ) ;
163- } else if ( rawValue . startsWith ( "[" ) || rawValue . startsWith ( "{" ) ) {
164- // Inline JSON array or object
165- try {
166- parent [ key ] = JSON . parse ( rawValue ) ;
167- } catch {
168- parent [ key ] = rawValue ;
169- }
170- } else {
171- parent [ key ] = parseScalar ( rawValue ) ;
172- }
173- continue ;
174- }
175- }
176-
177- return root ;
178- }
179-
180- function parseInlineMap ( text : string , target : Record < string , unknown > ) : void {
181- // Parse "key: value" pairs separated by newlines or within a single line
182- const parts = text . split ( / , \s * / ) ;
183- for ( const part of parts ) {
184- const m = part . match ( / ^ ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) \s * : \s * ( .+ ) $ / ) ;
185- if ( m ) {
186- target [ m [ 1 ] ] = parseScalar ( m [ 2 ] . trim ( ) ) ;
187- }
188- }
189- }
190-
191- function parseScalar ( value : string ) : string | number | boolean {
192- if ( value === "true" ) return true ;
193- if ( value === "false" ) return false ;
194- if ( value === "null" || value === "~" ) return "" ;
195- // Strip surrounding quotes
196- if (
197- ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) ||
198- ( value . startsWith ( "'" ) && value . endsWith ( "'" ) )
199- ) {
200- return value . slice ( 1 , - 1 ) ;
201- }
202- const num = Number ( value ) ;
203- if ( ! isNaN ( num ) && value !== "" ) return num ;
204- return value ;
17+ const result = yaml . load ( text ) ;
18+ if ( result === null || result === undefined ) return { } ;
19+ if ( typeof result !== "object" || Array . isArray ( result ) ) return { } ;
20+ return result as Record < string , unknown > ;
20521}
20622
20723// ---------------------------------------------------------------------------
0 commit comments