1919 *
2020 */
2121
22- // Parts of this file is adapted from https://github.com/cfjedimaster/brackets-jshint
22+ /* global path*/
2323
2424/**
2525 * Provides JSLint results via the core linting extension point
@@ -28,16 +28,26 @@ define(function (require, exports, module) {
2828
2929 // Load dependent modules
3030 const CodeInspection = brackets . getModule ( "language/CodeInspection" ) ,
31+ AppInit = brackets . getModule ( "utils/AppInit" ) ,
3132 Strings = brackets . getModule ( "strings" ) ,
33+ StringUtils = brackets . getModule ( "utils/StringUtils" ) ,
34+ FileSystemError = brackets . getModule ( "filesystem/FileSystemError" ) ,
35+ DocumentManager = brackets . getModule ( "document/DocumentManager" ) ,
3236 EditorManager = brackets . getModule ( "editor/EditorManager" ) ,
3337 ProjectManager = brackets . getModule ( "project/ProjectManager" ) ,
3438 PreferencesManager = brackets . getModule ( "preferences/PreferencesManager" ) ,
39+ Metrics = brackets . getModule ( "utils/Metrics" ) ,
40+ FileSystem = brackets . getModule ( "filesystem/FileSystem" ) ,
3541 IndexingWorker = brackets . getModule ( "worker/IndexingWorker" ) ;
3642
3743 IndexingWorker . loadScriptInWorker ( `${ module . uri } /../worker/html-worker.js` ) ;
3844
3945 const prefs = PreferencesManager . getExtensionPrefs ( "HTMLLint" ) ;
4046 const PREFS_HTML_LINT_DISABLED = "disabled" ;
47+ const CONFIG_FILE_NAME = ".htmlvalidate.json" ;
48+ const UNSUPPORTED_CONFIG_FILES = [ ".htmlvalidate.js" , ".htmlvalidate.cjs" ] ;
49+
50+ let projectSpecificOptions , configErrorMessage , configID = 0 ;
4151
4252 prefs . definePreference ( PREFS_HTML_LINT_DISABLED , "boolean" , false , {
4353 description : Strings . DESCRIPTION_HTML_LINT_DISABLE
@@ -54,15 +64,30 @@ define(function (require, exports, module) {
5464 }
5565 }
5666
67+ function _getLinterConfigFileErrorMsg ( ) {
68+ return [ {
69+ // JSLint returns 1-based line/col numbers
70+ pos : { line : - 1 , ch : 0 } ,
71+ message : configErrorMessage ,
72+ type : CodeInspection . Type . ERROR
73+ } ] ;
74+ }
75+
5776 /**
5877 * Run JSLint on the current document. Reports results to the main UI. Displays
5978 * a gold star when no errors are found.
6079 */
6180 async function lintOneFile ( text , fullPath ) {
6281 return new Promise ( ( resolve , reject ) => {
82+ if ( configErrorMessage ) {
83+ resolve ( { errors : _getLinterConfigFileErrorMsg ( ) } ) ;
84+ return ;
85+ }
6386 IndexingWorker . execPeer ( "htmlLint" , {
6487 text,
65- filePath : fullPath
88+ filePath : fullPath ,
89+ configID,
90+ config : projectSpecificOptions
6691 } ) . then ( lintResult => {
6792 const editor = EditorManager . getCurrentFullEditor ( ) ;
6893 if ( ! editor || editor . document . file . fullPath !== fullPath ) {
@@ -88,6 +113,110 @@ define(function (require, exports, module) {
88113 } ) ;
89114 }
90115
116+ function _readConfig ( dir ) {
117+ return new Promise ( ( resolve , reject ) => {
118+ const configFilePath = path . join ( dir , CONFIG_FILE_NAME ) ;
119+ let displayPath = ProjectManager . getProjectRelativeOrDisplayPath ( configFilePath ) ;
120+ DocumentManager . getDocumentForPath ( configFilePath ) . done ( function ( configDoc ) {
121+ let config ;
122+ const content = configDoc . getText ( ) ;
123+ try {
124+ config = JSON . parse ( content ) ;
125+ console . log ( "html-lint: loaded config file for project " + configFilePath ) ;
126+ } catch ( e ) {
127+ console . log ( "html-lint: error parsing " + configFilePath , content , e ) ;
128+ // just log and return as this is an expected failure for us while the user edits code
129+ reject ( StringUtils . format ( Strings . HTML_LINT_CONFIG_JSON_ERROR , displayPath ) ) ;
130+ return ;
131+ }
132+ resolve ( config ) ;
133+ } ) . fail ( ( err ) => {
134+ if ( err === FileSystemError . NOT_FOUND ) {
135+ resolve ( null ) ; // no config file is a valid case. we just resolve with null
136+ return ;
137+ }
138+ console . error ( "Error reading JSHint Config File" , configFilePath , err ) ;
139+ reject ( "Error reading JSHint Config File" , displayPath ) ;
140+ } ) ;
141+ } ) ;
142+ }
143+
144+ async function _validateUnsupportedConfig ( scanningProjectPath ) {
145+ let errorMessage ;
146+ for ( let unsupportedFileName of UNSUPPORTED_CONFIG_FILES ) {
147+ let exists = await FileSystem . existsAsync ( path . join ( scanningProjectPath , unsupportedFileName ) ) ;
148+ if ( exists ) {
149+ errorMessage = StringUtils . format ( Strings . HTML_LINT_CONFIG_UNSUPPORTED , unsupportedFileName ) ;
150+ break ;
151+ }
152+ }
153+ if ( scanningProjectPath !== ProjectManager . getProjectRoot ( ) . fullPath ) {
154+ // this is a rare race condition where the user switches project between the config reload
155+ // Eg. in integ tests. do nothing as another scan for the new project will be in progress.
156+ return ;
157+ }
158+ configErrorMessage = errorMessage ;
159+ CodeInspection . requestRun ( Strings . HTML_LINT_NAME ) ;
160+ }
161+
162+ function _reloadOptions ( ) {
163+ projectSpecificOptions = null ;
164+ configErrorMessage = null ;
165+ const scanningProjectPath = ProjectManager . getProjectRoot ( ) . fullPath ;
166+ configID ++ ;
167+ _readConfig ( scanningProjectPath , CONFIG_FILE_NAME ) . then ( ( config ) => {
168+ configID ++ ;
169+ if ( scanningProjectPath !== ProjectManager . getProjectRoot ( ) . fullPath ) {
170+ // this is a rare race condition where the user switches project between the get document call.
171+ // Eg. in integ tests. do nothing as another scan for the new project will be in progress.
172+ return ;
173+ }
174+ if ( config ) {
175+ Metrics . countEvent ( Metrics . EVENT_TYPE . LINT , "html" , "configPresent" ) ;
176+ projectSpecificOptions = config ;
177+ configErrorMessage = null ;
178+ CodeInspection . requestRun ( Strings . HTML_LINT_NAME ) ;
179+ } else {
180+ _validateUnsupportedConfig ( scanningProjectPath )
181+ . catch ( console . error ) ;
182+ }
183+ } ) . catch ( ( err ) => {
184+ configID ++ ;
185+ if ( scanningProjectPath !== ProjectManager . getProjectRoot ( ) . fullPath ) {
186+ return ;
187+ }
188+ Metrics . countEvent ( Metrics . EVENT_TYPE . LINT , "HTMLConfig" , "error" ) ;
189+ configErrorMessage = err ;
190+ CodeInspection . requestRun ( Strings . HTML_LINT_NAME ) ;
191+ } ) ;
192+ }
193+
194+ function _isFileInArray ( pathToMatch , filePathArray ) {
195+ if ( ! filePathArray ) {
196+ return false ;
197+ }
198+ for ( let filePath of filePathArray ) {
199+ if ( filePath === pathToMatch ) {
200+ return true ;
201+ }
202+ }
203+ return false ;
204+ }
205+
206+ function _projectFileChanged ( _evt , changedPath , added , removed ) {
207+ let configFilePath = path . join ( ProjectManager . getProjectRoot ( ) . fullPath , CONFIG_FILE_NAME ) ;
208+ if ( changedPath === configFilePath
209+ || _isFileInArray ( configFilePath , added ) || _isFileInArray ( configFilePath , removed ) ) {
210+ _reloadOptions ( ) ;
211+ }
212+ }
213+
214+ AppInit . appReady ( function ( ) {
215+ ProjectManager . on ( ProjectManager . EVENT_PROJECT_PATH_CHANGED_OR_RENAMED , _projectFileChanged ) ;
216+ ProjectManager . on ( ProjectManager . EVENT_PROJECT_OPEN , _reloadOptions ) ;
217+ _reloadOptions ( ) ;
218+ } ) ;
219+
91220 CodeInspection . register ( "html" , {
92221 name : Strings . HTML_LINT_NAME ,
93222 scanFileAsync : lintOneFile ,
0 commit comments