44This hook applies editorconfig formatting rules after file edits. 
55""" 
66
7+ import  hashlib 
78import  json 
89import  os 
910import  subprocess 
1011import  sys 
12+ import  time 
1113from  pathlib  import  Path 
1214
1315
@@ -27,37 +29,152 @@ def is_source_file(file_path):
2729    return  path .suffix .lower () in  extensions 
2830
2931
32+ def  get_file_hash (file_path ):
33+     """Get SHA256 hash of file contents""" 
34+     try :
35+         with  open (file_path , 'rb' ) as  f :
36+             return  hashlib .sha256 (f .read ()).hexdigest ()
37+     except  Exception :
38+         return  None 
39+ 
40+ 
41+ def  detect_formatting_changes (file_path , before_hash , after_hash ):
42+     """Detect what types of formatting changes were made""" 
43+     if  before_hash  ==  after_hash :
44+         return  []
45+ 
46+     changes  =  []
47+ 
48+     try :
49+         # Read the file to analyze changes 
50+         with  open (file_path , 'r' , encoding = 'utf-8' , errors = 'ignore' ) as  f :
51+             content  =  f .read ()
52+ 
53+         # Detect common formatting changes 
54+         if  content .endswith ('\n ' ) and  not  content .endswith ('\n \n ' ):
55+             changes .append ('final_newline' )
56+ 
57+         if  '\t '  in  content :
58+             changes .append ('indentation' )
59+ 
60+         if  content  !=  content .rstrip ():
61+             changes .append ('trailing_whitespace' )
62+ 
63+         if  '\r \n '  in  content :
64+             changes .append ('line_endings' )
65+ 
66+         # If we can't detect specific changes, just note something changed 
67+         if  not  changes :
68+             changes .append ('formatting' )
69+ 
70+     except  Exception :
71+         changes  =  ['formatting' ]
72+ 
73+     return  changes 
74+ 
75+ 
76+ def  get_eclint_version ():
77+     """Get eclint version if available""" 
78+     try :
79+         result  =  subprocess .run (['eclint' , '--version' ], capture_output = True , text = True )
80+         if  result .returncode  ==  0 :
81+             return  result .stdout .strip ()
82+     except  Exception :
83+         pass 
84+     return  None 
85+ 
86+ 
3087def  format_with_editorconfig (file_path ):
3188    """Apply editorconfig formatting to a file""" 
89+     start_time  =  time .time ()
90+     eclint_installed  =  False 
91+ 
3292    try :
93+         # Get file hash before formatting 
94+         before_hash  =  get_file_hash (file_path )
95+ 
3396        # Check if eclint is available 
3497        result  =  subprocess .run (['which' , 'eclint' ], capture_output = True , text = True )
3598        if  result .returncode  !=  0 :
3699            print ("eclint not found. Installing via npm..." , file = sys .stderr )
100+             install_start  =  time .time ()
37101            install_result  =  subprocess .run (['npm' , 'install' , '-g' , 'eclint' ],
38102                                          capture_output = True , text = True )
39103            if  install_result .returncode  !=  0 :
40-                 return  False , "Failed to install eclint" 
104+                 execution_time  =  time .time () -  start_time 
105+                 return  {
106+                     'success' : False ,
107+                     'error' : 'Failed to install eclint' ,
108+                     'error_details' : install_result .stderr ,
109+                     'execution_time' : execution_time ,
110+                     'suggestions' : [
111+                         'Install Node.js and npm if not available' ,
112+                         'Check npm global install permissions' ,
113+                         'Try: sudo npm install -g eclint' 
114+                     ]
115+                 }
116+             eclint_installed  =  True 
117+             print (f"eclint installed successfully in { time .time () -  install_start :.1f}  s" , file = sys .stderr )
41118
42119        # Apply editorconfig formatting 
43120        format_result  =  subprocess .run (['eclint' , 'fix' , file_path ],
44121                                     capture_output = True , text = True )
45122
123+         # Get file hash after formatting 
124+         after_hash  =  get_file_hash (file_path )
125+         execution_time  =  time .time () -  start_time 
126+ 
46127        if  format_result .returncode  ==  0 :
47-             return  True , f"Applied editorconfig formatting to { file_path }  " 
128+             changes  =  detect_formatting_changes (file_path , before_hash , after_hash )
129+             eclint_version  =  get_eclint_version ()
130+ 
131+             return  {
132+                 'success' : True ,
133+                 'changes_applied' : changes ,
134+                 'changes_made' : len (changes ) >  0 ,
135+                 'execution_time' : execution_time ,
136+                 'eclint_installed' : eclint_installed ,
137+                 'eclint_version' : eclint_version ,
138+                 'file_changed' : before_hash  !=  after_hash 
139+             }
48140        else :
49-             return  False , f"eclint failed: { format_result .stderr }  " 
141+             return  {
142+                 'success' : False ,
143+                 'error' : 'eclint formatting failed' ,
144+                 'error_details' : format_result .stderr ,
145+                 'execution_time' : execution_time ,
146+                 'suggestions' : [
147+                     'Check .editorconfig file syntax' ,
148+                     'Verify file permissions' ,
149+                     'Review eclint documentation' 
150+                 ]
151+             }
50152
51153    except  Exception  as  e :
52-         return  False , f"Error formatting file: { str (e )}  " 
154+         execution_time  =  time .time () -  start_time 
155+         return  {
156+             'success' : False ,
157+             'error' : f'Exception during formatting: { str (e )}  ' ,
158+             'execution_time' : execution_time ,
159+             'suggestions' : [
160+                 'Check file exists and is readable' ,
161+                 'Verify system permissions' ,
162+                 'Review hook configuration' 
163+             ]
164+         }
53165
54166
55167def  main ():
56168    try :
57169        input_data  =  json .load (sys .stdin )
58170    except  json .JSONDecodeError  as  e :
59-         print (f"Error: Invalid JSON input: { e }  " , file = sys .stderr )
60-         sys .exit (1 )
171+         error_output  =  {
172+             "decision" : "block" ,
173+             "reason" : f"EditorConfig hook received invalid JSON input: { e }  " ,
174+             "suggestions" : ["Check Claude Code hook configuration" ]
175+         }
176+         print (json .dumps (error_output ))
177+         sys .exit (0 )
61178
62179    hook_event  =  input_data .get ("hook_event_name" , "" )
63180    tool_name  =  input_data .get ("tool_name" , "" )
@@ -75,20 +192,56 @@ def main():
75192    if  not  os .path .exists (file_path ):
76193        sys .exit (0 )
77194
78-     success , message  =  format_with_editorconfig (file_path )
195+     # Apply editorconfig formatting with enhanced reporting 
196+     result  =  format_with_editorconfig (file_path )
197+     filename  =  os .path .basename (file_path )
198+ 
199+     if  result ['success' ]:
200+         # Generate rich success message 
201+         if  result ['changes_made' ]:
202+             changes_text  =  ', ' .join (result ['changes_applied' ])
203+             time_text  =  f" ({ result ['execution_time' ]:.1f}  s)" 
204+             system_message  =  f"✓ EditorConfig: { changes_text }   applied to { filename } { time_text }  " 
205+         else :
206+             time_text  =  f" ({ result ['execution_time' ]:.1f}  s)" 
207+             system_message  =  f"✓ EditorConfig: no changes needed for { filename } { time_text }  " 
208+ 
209+         # Add installation note if eclint was installed 
210+         if  result ['eclint_installed' ]:
211+             system_message  +=  " (eclint auto-installed)" 
79212
80-     if  success :
81-         # Use JSON output to suppress the normal stdout display 
82213        output  =  {
83214            "suppressOutput" : True ,
84-             "systemMessage" : f"✓ EditorConfig formatting applied to { os .path .basename (file_path )}  " 
215+             "systemMessage" : system_message ,
216+             "formattingResults" : {
217+                 "changesApplied" : result ['changes_applied' ],
218+                 "changesMade" : result ['changes_made' ],
219+                 "executionTime" : result ['execution_time' ],
220+                 "eclintInstalled" : result ['eclint_installed' ],
221+                 "eclintVersion" : result .get ('eclint_version' ),
222+                 "fileChanged" : result ['file_changed' ]
223+             }
85224        }
86225        print (json .dumps (output ))
87226        sys .exit (0 )
88227    else :
89-         # Non-blocking error - show message but don't fail 
90-         print (f"Warning: { message }  " , file = sys .stderr )
91-         sys .exit (1 )
228+         # Enhanced error output with structured information 
229+         error_message  =  f"EditorConfig formatting failed for { filename }  : { result ['error' ]}  " 
230+ 
231+         output  =  {
232+             "decision" : "block" ,
233+             "reason" : error_message ,
234+             "stopReason" : f"Code formatting issues in { filename }  " ,
235+             "formattingError" : {
236+                 "errorType" : result ['error' ],
237+                 "errorDetails" : result .get ('error_details' , '' ),
238+                 "executionTime" : result ['execution_time' ],
239+                 "suggestions" : result .get ('suggestions' , []),
240+                 "filename" : filename 
241+             }
242+         }
243+         print (json .dumps (output ))
244+         sys .exit (0 )
92245
93246
94247if  __name__  ==  "__main__" :
0 commit comments