@@ -491,6 +491,105 @@ 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 = 500
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 = f"dts_linter_{ i } .patch"
540
+ temp_patch_files .append (temp_patch )
541
+
542
+ cmd = [
543
+ "npx" , "--no" , "dts-linter" , "--" ,
544
+ "--outputFormat" , "json" ,
545
+ "--format" ,
546
+ "--patchFile" , temp_patch ,
547
+ ]
548
+ for file in batch :
549
+ cmd .extend (["--file" , file ])
550
+
551
+ try :
552
+ json_output = self ._parse_json_output (cmd )
553
+
554
+ if json_output and "issues" in json_output :
555
+ cwd = json_output .get ("cwd" , "" )
556
+ logging .info (f"Processing issues from: { cwd } " )
557
+
558
+ for issue in json_output ["issues" ]:
559
+ level = issue .get ("level" , "unknown" )
560
+ message = issue .get ("message" , "" )
561
+
562
+ if level == "info" :
563
+ logging .info (message )
564
+ else :
565
+ title = issue .get ("title" , "" )
566
+ file = issue .get ("file" , "" )
567
+ line = issue .get ("startLine" , None )
568
+ col = issue .get ("startCol" , None )
569
+ end_line = issue .get ("endLine" , None )
570
+ end_col = issue .get ("endCol" , None )
571
+ self .fmtd_failure (level , title , file , line , col , message , end_line , end_col )
572
+
573
+ except subprocess .CalledProcessError as ex :
574
+ stderr_output = ex .stderr if ex .stderr else ""
575
+ if stderr_output .strip ():
576
+ self .failure (f"dts-linter found issues:\n { stderr_output } " )
577
+ else :
578
+ self .failure ("dts-linter failed with no output. "
579
+ "Make sure you install Node.JS and then run npm ci inside ZEPHYR_BASE" )
580
+ except RuntimeError as ex :
581
+ self .failure (f"{ ex } " )
582
+
583
+ # merge all temp patch files into one
584
+ with open ("dts_linter.patch" , "wb" ) as final_patch :
585
+ for patch in temp_patch_files :
586
+ with open (patch , "rb" ) as f :
587
+ shutil .copyfileobj (f , final_patch )
588
+
589
+ # cleanup
590
+ for patch in temp_patch_files :
591
+ os .remove (patch )
592
+
494
593
class KconfigCheck (ComplianceTest ):
495
594
"""
496
595
Checks is we are introducing any new warnings/errors with Kconfig,
0 commit comments