microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/linting/Invoke-JsonLint.ps1

295lines Ā· modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#Requires -Version 7.0
5<#
6.SYNOPSIS
7 Validates JSON files for strict well-formedness using System.Text.Json.
8
9.DESCRIPTION
10 Parses each target JSON file with System.Text.Json.JsonDocument, which rejects
11 trailing commas, comments, and trailing content after the root value (such as
12 accidentally concatenated objects). This catches malformed JSON that the more
13 lenient ConvertFrom-Json silently accepts.
14
15 By default it lints schema and fixture JSON under scripts/linting/schemas and
16 scripts/tests/fixtures. Supports changed-files-only mode for PR validation and
17 exports JSON results for CI integration.
18
19 Fixtures whose file name matches an ExcludePatterns entry (by default any
20 invalid-*.json) are skipped, since some fixtures are intentionally malformed to
21 exercise parser error handling.
22
23.PARAMETER Paths
24 Directories (or files) to lint. Defaults to the schema and fixture trees.
25
26.PARAMETER ExcludePatterns
27 File-name wildcard patterns to skip. Defaults to intentionally-invalid fixtures.
28
29.PARAMETER ChangedFilesOnly
30 Validate only changed JSON files within the target paths.
31
32.PARAMETER BaseBranch
33 Base branch for detecting changed files (default: origin/main).
34
35.PARAMETER OutputPath
36 Path for JSON results output (default: logs/json-lint-results.json).
37
38.EXAMPLE
39 ./scripts/linting/Invoke-JsonLint.ps1 -Verbose
40
41.EXAMPLE
42 ./scripts/linting/Invoke-JsonLint.ps1 -ChangedFilesOnly
43
44.NOTES
45 Requires no external tooling; uses the .NET System.Text.Json parser bundled
46 with PowerShell 7.
47#>
48
49[CmdletBinding()]
50param(
51 [Parameter(Mandatory = $false)]
52 [string[]]$Paths = @('scripts/linting/schemas', 'scripts/tests/fixtures'),
53
54 [Parameter(Mandatory = $false)]
55 [string[]]$ExcludePatterns = @('invalid-*.json'),
56
57 [Parameter(Mandatory = $false)]
58 [switch]$ChangedFilesOnly,
59
60 [Parameter(Mandatory = $false)]
61 [string]$BaseBranch = "origin/main",
62
63 [Parameter(Mandatory = $false)]
64 [string]$OutputPath = "logs/json-lint-results.json"
65)
66
67$ErrorActionPreference = 'Stop'
68
69# Import shared helpers
70Import-Module (Join-Path $PSScriptRoot "Modules/LintingHelpers.psm1") -Force
71Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force
72
73#region Functions
74
75function Test-JsonFile {
76 <#
77 .SYNOPSIS
78 Strictly parses a JSON file and returns an issue object when invalid.
79 #>
80 [CmdletBinding()]
81 [OutputType([hashtable])]
82 param(
83 [Parameter(Mandatory = $true)]
84 [string]$Path
85 )
86
87 try {
88 $content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
89 }
90 catch {
91 return @{
92 File = $Path
93 Line = 0
94 Column = 0
95 Message = "Unable to read file: $($_.Exception.Message)"
96 }
97 }
98
99 if ([string]::IsNullOrWhiteSpace($content)) {
100 return @{
101 File = $Path
102 Line = 0
103 Column = 0
104 Message = "File is empty or contains only whitespace"
105 }
106 }
107
108 # JsonDocumentOptions defaults are strict: no trailing commas, comments disallowed.
109 $options = [System.Text.Json.JsonDocumentOptions]::new()
110 try {
111 $document = [System.Text.Json.JsonDocument]::Parse($content, $options)
112 $document.Dispose()
113 return $null
114 }
115 catch [System.Text.Json.JsonException] {
116 $jsonError = $_.Exception
117 return @{
118 File = $Path
119 Line = if ($null -ne $jsonError.LineNumber) { [int]$jsonError.LineNumber + 1 } else { 0 }
120 Column = if ($null -ne $jsonError.BytePositionInLine) { [int]$jsonError.BytePositionInLine + 1 } else { 0 }
121 Message = $jsonError.Message
122 }
123 }
124 catch {
125 return @{
126 File = $Path
127 Line = 0
128 Column = 0
129 Message = $_.Exception.Message
130 }
131 }
132}
133
134function Invoke-JsonLintCore {
135 [CmdletBinding()]
136 [OutputType([void])]
137 param(
138 [Parameter(Mandatory = $false)]
139 [string[]]$Paths = @('scripts/linting/schemas', 'scripts/tests/fixtures'),
140
141 [Parameter(Mandatory = $false)]
142 [string[]]$ExcludePatterns = @('invalid-*.json'),
143
144 [Parameter(Mandatory = $false)]
145 [switch]$ChangedFilesOnly,
146
147 [Parameter(Mandatory = $false)]
148 [string]$BaseBranch = "origin/main",
149
150 [Parameter(Mandatory = $false)]
151 [string]$OutputPath = "logs/json-lint-results.json"
152 )
153
154 Write-Host "šŸ” Running JSON Lint (System.Text.Json strict parse)..." -ForegroundColor Cyan
155
156 # Get files to analyze
157 $filesToAnalyze = @()
158
159 if ($ChangedFilesOnly) {
160 Write-Host "Detecting changed JSON files..." -ForegroundColor Cyan
161 $changedFiles = @(Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.json'))
162 $filesToAnalyze = @($changedFiles | Where-Object {
163 $candidate = $_
164 ($Paths | Where-Object { $candidate -like "$_/*" -or $candidate -eq $_ }).Count -gt 0
165 })
166 }
167 else {
168 Write-Host "Analyzing all JSON files..." -ForegroundColor Cyan
169 foreach ($targetPath in $Paths) {
170 if (-not (Test-Path $targetPath)) {
171 Write-Verbose "Skipping missing path: $targetPath"
172 continue
173 }
174
175 if (Test-Path $targetPath -PathType Leaf) {
176 if ($targetPath -like '*.json') { $filesToAnalyze += $targetPath }
177 continue
178 }
179
180 $filesToAnalyze += @(
181 Get-ChildItem -Path $targetPath -File -Recurse |
182 Where-Object { $_.Extension -eq '.json' } |
183 ForEach-Object { $_.FullName }
184 )
185 }
186 }
187
188 if ($ExcludePatterns -and $ExcludePatterns.Count -gt 0) {
189 $filesToAnalyze = @($filesToAnalyze | Where-Object {
190 $leaf = Split-Path $_ -Leaf
191 -not @($ExcludePatterns | Where-Object { $leaf -like $_ }).Count
192 })
193 }
194
195 $filesToAnalyze = @($filesToAnalyze | Sort-Object -Unique)
196
197 if (@($filesToAnalyze).Count -eq 0) {
198 Write-Host "āœ… No JSON files to analyze" -ForegroundColor Green
199 Set-CIOutput -Name "count" -Value "0"
200 Set-CIOutput -Name "issues" -Value "0"
201 return
202 }
203
204 Write-Host "Analyzing $($filesToAnalyze.Count) JSON files..." -ForegroundColor Cyan
205 Set-CIOutput -Name "count" -Value $filesToAnalyze.Count
206
207 # Validate each file
208 $issues = @()
209 foreach ($file in $filesToAnalyze) {
210 $issue = Test-JsonFile -Path $file
211 if ($null -ne $issue) {
212 $issues += $issue
213
214 Write-CIAnnotation `
215 -Message $issue.Message `
216 -Level Error `
217 -File $issue.File `
218 -Line $issue.Line `
219 -Column $issue.Column
220
221 Write-Host " āŒ $($issue.File):$($issue.Line):$($issue.Column): $($issue.Message)" -ForegroundColor Red
222 }
223 }
224
225 $hasErrors = $issues.Count -gt 0
226
227 # Export results
228 $summary = @{
229 TotalFiles = $filesToAnalyze.Count
230 TotalIssues = $issues.Count
231 Errors = $issues.Count
232 Warnings = 0
233 HasErrors = $hasErrors
234 Timestamp = Get-StandardTimestamp
235 Tool = "System.Text.Json"
236 }
237
238 # Ensure logs directory exists
239 $logsDir = Split-Path $OutputPath -Parent
240 if ($logsDir -and -not (Test-Path $logsDir)) {
241 New-Item -ItemType Directory -Force -Path $logsDir | Out-Null
242 }
243
244 $issues | ConvertTo-Json -Depth 5 | Out-File $OutputPath
245 $summary | ConvertTo-Json | Out-File "logs/json-lint-summary.json"
246
247 # Set outputs
248 Set-CIOutput -Name "issues" -Value $summary.TotalIssues
249 Set-CIOutput -Name "errors" -Value $summary.Errors
250
251 if ($hasErrors) {
252 Set-CIEnv -Name "JSON_LINT_FAILED" -Value "true"
253 }
254
255 # Write summary
256 Write-CIStepSummary -Content "## JSON Lint Results`n"
257
258 if ($summary.TotalIssues -eq 0) {
259 Write-CIStepSummary -Content "āœ… **Status**: Passed`n`nAll $($summary.TotalFiles) JSON files passed validation."
260 Write-Host "`nāœ… All JSON files passed strict validation!" -ForegroundColor Green
261 return
262 }
263 else {
264 Write-CIStepSummary -Content @"
265āŒ **Status**: Failed
266
267| Metric | Count |
268|--------|-------|
269| Files Analyzed | $($summary.TotalFiles) |
270| Total Issues | $($summary.TotalIssues) |
271| Errors | $($summary.Errors) |
272"@
273
274 Write-Host "`nāŒ JSON Lint found $($summary.TotalIssues) issue(s)" -ForegroundColor Red
275 throw "JSON Lint found $($summary.TotalIssues) issue(s)"
276 }
277}
278
279#endregion Functions
280
281#region Main Execution
282
283if ($MyInvocation.InvocationName -ne '.') {
284 try {
285 Invoke-JsonLintCore -Paths $Paths -ExcludePatterns $ExcludePatterns -ChangedFilesOnly:$ChangedFilesOnly -BaseBranch $BaseBranch -OutputPath $OutputPath
286 exit 0
287 }
288 catch {
289 Write-Error -ErrorAction Continue "JSON Lint failed: $($_.Exception.Message)"
290 Write-CIAnnotation -Message $_.Exception.Message -Level Error
291 exit 1
292 }
293}
294
295#endregion Main Execution
296