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