-
Notifications
You must be signed in to change notification settings - Fork 7.6k
Fix #5137 async linting #6530
Fix #5137 async linting #6530
Changes from all commits
835b3af
db33c0f
e730595
d9deaa1
b91ec3f
08e0392
03c9ed3
5ec5534
9fc0425
62933ce
d684e93
0c091df
fe7d342
a329181
7435877
fecb543
563263d
6f5e030
c786e69
a075ab4
a46b9df
c3411bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,7 +76,8 @@ define(function (require, exports, module) { | |
* Constants for the preferences defined in this file. | ||
*/ | ||
var PREF_ENABLED = "enabled", | ||
PREF_COLLAPSED = "collapsed"; | ||
PREF_COLLAPSED = "collapsed", | ||
PREF_ASYNC_TIMEOUT = "asyncTimeout"; | ||
|
||
var prefs = PreferencesManager.getExtensionPrefs("linting"); | ||
|
||
|
@@ -120,7 +121,7 @@ define(function (require, exports, module) { | |
|
||
/** | ||
* @private | ||
* @type {Object.<languageId:string, Array.<{name:string, scanFile:function(string, string):Object}>>} | ||
* @type {{languageId:string, Array.<{name:string, scanFileAsync:?function(string, string):!{$.Promise}, scanFile:?function(string, string):Object}>}} | ||
*/ | ||
var _providers = {}; | ||
|
||
|
@@ -129,6 +130,15 @@ define(function (require, exports, module) { | |
* @type {boolean} | ||
*/ | ||
var _hasErrors; | ||
|
||
/** | ||
* Promise of the returned by the last call to inspectFile or null if linting is disabled. Used to prevent any stale promises | ||
* to cause updates of the UI. | ||
* | ||
* @private | ||
* @type {$.Promise} | ||
*/ | ||
var _currentPromise = null; | ||
|
||
/** | ||
* Enable or disable the "Go to First Error" command | ||
|
@@ -148,7 +158,7 @@ define(function (require, exports, module) { | |
* Decision is made depending on the file extension. | ||
* | ||
* @param {!string} filePath | ||
* @return ?{Array.<{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}}>} provider | ||
* @return ?{Array.<{name:string, scanFileAsync:?function(string, string):!{$.Promise}, scanFile:?function(string, string):?{errors:!Array, aborted:boolean}}>} provider | ||
*/ | ||
function getProvidersForPath(filePath) { | ||
return _providers[LanguageManager.getLanguageForPath(filePath).getId()]; | ||
|
@@ -166,7 +176,7 @@ define(function (require, exports, module) { | |
* If there are no providers registered for this file, the Promise yields null instead. | ||
* | ||
* @param {!File} file File that will be inspected for errors. | ||
* @param {?Array.<{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}}>} providerList | ||
* @param {?Array.<{name:string, scanFileAsync:?function(string, string):!{$.Promise}, scanFile:?function(string, string):?{errors:!Array, aborted:boolean}}>} providerList | ||
* @return {$.Promise} a jQuery promise that will be resolved with ?Array.<{provider:Object, result: ?{errors:!Array, aborted:boolean}}> | ||
*/ | ||
function inspectFile(file, providerList) { | ||
|
@@ -179,29 +189,72 @@ define(function (require, exports, module) { | |
response.resolve(null); | ||
return response.promise(); | ||
} | ||
|
||
DocumentManager.getDocumentText(file) | ||
.done(function (fileText) { | ||
var perfTimerInspector = PerfUtils.markStart("CodeInspection:\t" + file.fullPath); | ||
|
||
providerList.forEach(function (provider) { | ||
var perfTimerProvider = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + file.fullPath); | ||
|
||
try { | ||
var scanResult = provider.scanFile(fileText, file.fullPath); | ||
var perfTimerInspector = PerfUtils.markStart("CodeInspection:\t" + file.fullPath), | ||
masterPromise; | ||
|
||
masterPromise = Async.doInParallel(providerList, function (provider) { | ||
var perfTimerProvider = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + file.fullPath), | ||
runPromise = new $.Deferred(); | ||
|
||
runPromise.done(function (scanResult) { | ||
results.push({provider: provider, result: scanResult}); | ||
} catch (err) { | ||
console.error("[CodeInspection] Provider " + provider.name + " threw an error: " + err); | ||
response.reject(err); | ||
return; | ||
}); | ||
|
||
if (provider.scanFileAsync) { | ||
window.setTimeout(function () { | ||
// timeout error | ||
var errTimeout = { | ||
pos: { line: -1, col: 0}, | ||
message: StringUtils.format(Strings.LINTER_TIMED_OUT, provider.name, prefs.get(PREF_ASYNC_TIMEOUT)), | ||
type: Type.ERROR | ||
}; | ||
runPromise.resolve({errors: [errTimeout]}); | ||
}, prefs.get(PREF_ASYNC_TIMEOUT)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think we should remove this timeout -- it breaks use cases like retrieving linting results from a web service, and it's something we don't do in any of our other async provider-based APIs. And I don't see a clear reason why we need it (especially with the race condition guard that makes us ignore stale promises, even if they dangle forever). If we must keep it, we should make the default timeout significantly longer and use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current model, any hanging request actually delays the entire rendering. I agree with you and Ingo that this is a fragile with regards to the linters which may take long, but it could be addressed incrementally (e.g. make results rendering incremental without re-running all the linters). It should not be holding up this PR. This is already way better than nothing. Existing linters can take advantage of this. This is all started just because I wanted to support recursive configuration file look up in JSHint which can be done right now. I don't see this is piece of code as a step in an opposite direction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand those are your needs, but there are multiple other people who've requested this specifically in order to get web services or node-side services into the picture. I don't think we can call #5137 fixed if we're timing out after just 1 sec. How about we just raise it to 5 or 10 sec for now? It also seems weird that this is a single global pref rather than an option linters can specify when registering, or a per-linter pref. I think we'll need to revisit that if there are complaints about the default timeout, but for now it seems ok (as long as we bump up the default here). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my case like I explained on IRC, my initial lint can take few seconds on large projects (while the subsequent will be faster). So if you go with the timeout a bigger one would make me happy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Let's make it 10 secs for now. I guess my PoV was limited to the uses cases I was exposed to. I filed #7397 which is the right way to deal with this entire timeout issue. |
||
provider.scanFileAsync(fileText, file.fullPath) | ||
.done(function (scanResult) { | ||
PerfUtils.addMeasurement(perfTimerProvider); | ||
runPromise.resolve(scanResult); | ||
}) | ||
.fail(function (err) { | ||
var errError = { | ||
pos: {line: -1, col: 0}, | ||
message: StringUtils.format(Strings.LINTER_FAILED, provider.name, err), | ||
type: Type.ERROR | ||
}; | ||
console.error("[CodeInspection] Provider " + provider.name + " (async) failed: " + err); | ||
runPromise.resolve({errors: [errError]}); | ||
}); | ||
} else { | ||
try { | ||
var scanResult = provider.scanFile(fileText, file.fullPath); | ||
PerfUtils.addMeasurement(perfTimerProvider); | ||
runPromise.resolve(scanResult); | ||
} catch (err) { | ||
var errError = { | ||
pos: {line: -1, col: 0}, | ||
message: StringUtils.format(Strings.LINTER_FAILED, provider.name, err), | ||
type: Type.ERROR | ||
}; | ||
console.error("[CodeInspection] Provider " + provider.name + " (sync) threw an error: " + err); | ||
runPromise.resolve({errors: [errError]}); | ||
} | ||
} | ||
return runPromise.promise(); | ||
|
||
PerfUtils.addMeasurement(perfTimerProvider); | ||
}, false); | ||
|
||
masterPromise.then(function () { | ||
// sync async may have pushed results in different order, restore the original order | ||
results.sort(function (a, b) { | ||
return providerList.indexOf(a.provider) - providerList.indexOf(b.provider); | ||
}); | ||
PerfUtils.addMeasurement(perfTimerInspector); | ||
response.resolve(results); | ||
}); | ||
|
||
PerfUtils.addMeasurement(perfTimerInspector); | ||
|
||
response.resolve(results); | ||
}) | ||
.fail(function (err) { | ||
console.error("[CodeInspection] Could not read file for inspection: " + file.fullPath); | ||
|
@@ -216,7 +269,7 @@ define(function (require, exports, module) { | |
* change based on the number of problems reported and how many provider reported problems. | ||
* | ||
* @param {Number} numProblems - total number of problems across all providers | ||
* @param {Array.<{name:string, scanFile:function(string, string):Object}>} providersReportingProblems - providers that reported problems | ||
* @param {Array.<{name:string, scanFileAsync:?function(string, string):!{$.Promise}, scanFile:?function(string, string):Object}>} providersReportingProblems - providers that reported problems | ||
* @param {boolean} aborted - true if any provider returned a result with the 'aborted' flag set | ||
*/ | ||
function updatePanelTitleAndStatusBar(numProblems, providersReportingProblems, aborted) { | ||
|
@@ -261,6 +314,7 @@ define(function (require, exports, module) { | |
function run() { | ||
if (!_enabled) { | ||
_hasErrors = false; | ||
_currentPromise = null; | ||
Resizer.hide($problemsPanel); | ||
StatusBar.updateIndicator(INDICATOR_ID, true, "inspection-disabled", Strings.LINT_DISABLED); | ||
setGotoEnabled(false); | ||
|
@@ -278,12 +332,12 @@ define(function (require, exports, module) { | |
var providersReportingProblems = []; | ||
|
||
// run all the providers registered for this file type | ||
inspectFile(currentDoc.file, providerList).then(function (results) { | ||
// check if current document wasn't changed while inspectFile was running | ||
if (currentDoc !== DocumentManager.getCurrentDocument()) { | ||
(_currentPromise = inspectFile(currentDoc.file, providerList)).then(function (results) { | ||
// check if promise has not changed while inspectFile was running | ||
if (this !== _currentPromise) { | ||
return; | ||
} | ||
|
||
// how many errors in total? | ||
var errors = results.reduce(function (a, item) { return a + (item.result ? item.result.errors.length : 0); }, 0); | ||
|
||
|
@@ -361,6 +415,7 @@ define(function (require, exports, module) { | |
} else { | ||
// No provider for current file | ||
_hasErrors = false; | ||
_currentPromise = null; | ||
Resizer.hide($problemsPanel); | ||
var language = currentDoc && LanguageManager.getLanguageForPath(currentDoc.file.fullPath); | ||
if (language) { | ||
|
@@ -380,9 +435,16 @@ define(function (require, exports, module) { | |
* Registering any provider for the "javascript" language automatically unregisters the built-in | ||
* Brackets JSLint provider. This is a temporary convenience until UI exists for disabling | ||
* registered providers. | ||
* | ||
* If provider implements both scanFileAsync and scanFile functions for asynchronous and synchronous | ||
* code inspection, respectively, the asynchronous version will take precedence and will be used to | ||
* perform code inspection. | ||
* | ||
* A code inspection provider's scanFileAsync must return a {$.Promise} object which should be | ||
* resolved with ?{errors:!Array, aborted:boolean}}. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add a sentence that makes it clear, that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somehow I added this note to |
||
* @param {string} languageId | ||
* @param {{name:string, scanFile:function(string, string):?{errors:!Array, aborted:boolean}} provider | ||
* @param {{name:string, scanFileAsync:?function(string, string):!{$.Promise}, scanFile:?function(string, string):?{errors:!Array, aborted:boolean}}} provider | ||
* | ||
* Each error is: { pos:{line,ch}, endPos:?{line,ch}, message:string, type:?Type } | ||
* If type is unspecified, Type.WARNING is assumed. | ||
|
@@ -391,15 +453,15 @@ define(function (require, exports, module) { | |
if (!_providers[languageId]) { | ||
_providers[languageId] = []; | ||
} | ||
|
||
if (languageId === "javascript") { | ||
// This is a special case to enable extension provider to replace the JSLint provider | ||
// in favor of their own implementation | ||
_.remove(_providers[languageId], function (registeredProvider) { | ||
return registeredProvider.name === "JSLint"; | ||
}); | ||
} | ||
|
||
_providers[languageId].push(provider); | ||
|
||
run(); // in case a file of this type is open currently | ||
|
@@ -508,7 +570,7 @@ define(function (require, exports, module) { | |
toggleCollapsed(prefs.get(PREF_COLLAPSED), true); | ||
}); | ||
|
||
|
||
prefs.definePreference(PREF_ASYNC_TIMEOUT, "number", 10000); | ||
|
||
// Initialize items dependent on HTML DOM | ||
AppInit.htmlReady(function () { | ||
|
@@ -571,12 +633,13 @@ define(function (require, exports, module) { | |
}); | ||
|
||
// Testing | ||
exports._unregisterAll = _unregisterAll; | ||
exports._unregisterAll = _unregisterAll; | ||
exports._PREF_ASYNC_TIMEOUT = PREF_ASYNC_TIMEOUT; | ||
|
||
// Public API | ||
exports.register = register; | ||
exports.Type = Type; | ||
exports.toggleEnabled = toggleEnabled; | ||
exports.inspectFile = inspectFile; | ||
exports.requestRun = run; | ||
exports.requestRun = run; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doInParallel() means the order of the providers in the results table will be nondeterministic. So this should either be sorted before display (e.g. alphabetically by provider name), or it should use doSequentially() so the order is stable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Added a test case for it also.