@@ -491,6 +491,106 @@ def required_false_check(self, binding):
491
491
"'required: false' is redundant, please remove"
492
492
)
493
493
494
+
495
+ class DevicetreeLintingCheck (ComplianceTest ):
496
+ """
497
+ Checks if we are introducing syntax or formatting issues to devicetree files.
498
+ """
499
+ name = "DevicetreeLinting"
500
+ doc = "See https://docs.zephyrproject.org/latest/contribute/style/devicetree.html for more details."
501
+
502
+ def _parse_json_output (self , cmd , cwd = None ):
503
+ """Run command and parse single JSON output with issues array"""
504
+ result = subprocess .run (
505
+ cmd ,
506
+ stdout = subprocess .PIPE ,
507
+ stderr = subprocess .STDOUT ,
508
+ check = False ,
509
+ text = True ,
510
+ cwd = cwd or GIT_TOP
511
+ )
512
+
513
+ if not result .stdout .strip ():
514
+ return None
515
+
516
+ try :
517
+ json_data = json .loads (result .stdout )
518
+ return json_data
519
+ except json .JSONDecodeError as e :
520
+ raise RuntimeError (f"Failed to parse dts-linter JSON output: { e } " )
521
+
522
+ def run (self ):
523
+ # Get changed DTS files
524
+ dts_files = [
525
+ file for file in get_files (filter = "d" )
526
+ if file .endswith ((".dts" , ".dtsi" , ".overlay" ))
527
+ ]
528
+
529
+ if not dts_files :
530
+ self .skip ('No DTS' )
531
+
532
+ temp_patch_files = []
533
+ batch_size = 100 # adjust as needed
534
+
535
+ for i in range (0 , len (dts_files ), batch_size ):
536
+ batch = dts_files [i :i + batch_size ]
537
+
538
+ # use a temporary file for each batch
539
+ temp_patch = tempfile .NamedTemporaryFile (delete = False , suffix = ".patch" )
540
+ temp_patch .close ()
541
+ temp_patch_files .append (temp_patch .name )
542
+
543
+ cmd = [
544
+ "npx" , "--no" , "dts-linter" ,
545
+ "--outputFormat" , "json" ,
546
+ "--format" ,
547
+ "--patchFile" , temp_patch .name ,
548
+ ]
549
+ for file in batch :
550
+ cmd .extend (["--file" , file ])
551
+
552
+ try :
553
+ json_output = self ._parse_json_output (cmd )
554
+
555
+ if json_output and "issues" in json_output :
556
+ cwd = json_output .get ("cwd" , "" )
557
+ logging .info (f"Processing issues from: { cwd } " )
558
+
559
+ for issue in json_output ["issues" ]:
560
+ level = issue .get ("level" , "unknown" )
561
+ message = issue .get ("message" , "" )
562
+
563
+ if level == "info" :
564
+ logging .info (message )
565
+ else :
566
+ title = issue .get ("title" , "" )
567
+ file = issue .get ("file" , "" )
568
+ line = issue .get ("startLine" , None )
569
+ col = issue .get ("startCol" , None )
570
+ end_line = issue .get ("endLine" , None )
571
+ end_col = issue .get ("endCol" , None )
572
+ self .fmtd_failure (level , title , file , line , col , message , end_line , end_col )
573
+
574
+ except subprocess .CalledProcessError as ex :
575
+ stderr_output = ex .stderr if ex .stderr else ""
576
+ if stderr_output .strip ():
577
+ self .failure (f"dts-linter found issues:\n { stderr_output } " )
578
+ else :
579
+ self .failure ("dts-linter failed with no output. "
580
+ "Make sure you install Node.JS and then run npm ci inside ZEPHYR_BASE" )
581
+ except RuntimeError as ex :
582
+ self .failure (f"{ ex } " )
583
+
584
+ # merge all temp patch files into one
585
+ with open ("dts_linter.patch" , "wb" ) as final_patch :
586
+ for patch in temp_patch_files :
587
+ with open (patch , "rb" ) as f :
588
+ shutil .copyfileobj (f , final_patch )
589
+
590
+ # cleanup
591
+ for patch in temp_patch_files :
592
+ os .remove (patch )
593
+
494
594
class KconfigCheck (ComplianceTest ):
495
595
"""
496
596
Checks is we are introducing any new warnings/errors with Kconfig,
0 commit comments