Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions $shared/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export type RuleCustomization = {
severity: RuleSeverity;
/** Only apply to autofixable rules */
fixable?: boolean;
/** Only apply when the file has unsaved changes */
unsavedOnly?: boolean;
};

export type RunValues = 'onType' | 'onSave';
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ This extension contributes the following variables to the [settings](https://cod
- Prefix the name with a `"!"` to target all rules that _don't_ match the name: `{ "rule": "!no-*", "severity": "info" }`
- `"severity"`: Sets a new severity for matched rule(s), `"downgrade"`s them to a lower severity, `"upgrade"`s them to a higher severity, or `"default"`s them to their original severity
- `"fixable"`: Select only autofixable rules: `{ "rule": "no-*", "fixable": true, "severity": "info" }`
- `"unsavedOnly"`: When `true`, only applies the customization while the file has unsaved changes (is dirty): `{ "rule": "...", "severity": "off", "unsavedOnly": true }`

In this example, all rules are overridden to warnings:

Expand Down Expand Up @@ -480,6 +481,14 @@ This extension contributes the following variables to the [settings](https://cod
]
```

In this example, `foo` is silenced while editing but shown once the file is saved:

```json
"eslint.rules.customizations": [
{ "rule": "foo", "severity": "off", "unsavedOnly": true }
]
```

- `eslint.format.enable` (@since 2.0.0) - uses ESlint as a formatter for files that are validated by ESLint. If enabled please ensure to disable other formatters if you want to make this the default. A good way to do so is to add the following setting `"[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }` for JavaScript. For TypeScript you need to add `"[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" }`.
- `eslint.onIgnoredFiles` (@since 2.0.10): used to control whether warnings should be generated when trying to lint ignored files. Default is `off`. Can be set to `warn`.
- `editor.codeActionsOnSave` (@since 2.0.0): this setting now supports an entry `source.fixAll.eslint`. If set to true all auto-fixable ESLint errors from all plugins will be fixed on save. You can also selectively enable and disabled specific languages using VS Code's language scoped settings. To disable `codeActionsOnSave` for HTML files use the following setting:
Expand Down
19 changes: 9 additions & 10 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,18 +823,17 @@ export namespace ESLintClient {

return rawConfig.map(rawValue => {
if (typeof rawValue.severity === 'string' && typeof rawValue.rule === 'string') {
const customization: RuleCustomization = {
severity: rawValue.severity,
rule: rawValue.rule,
};
if (typeof rawValue.fixable === 'boolean') {
return {
severity: rawValue.severity,
rule: rawValue.rule,
fixable: rawValue.fixable
};
} else {
return {
severity: rawValue.severity,
rule: rawValue.rule,
};
customization.fixable = rawValue.fixable;
}
if (typeof rawValue.unsavedOnly === 'boolean') {
customization.unsavedOnly = rawValue.unsavedOnly;
}
return customization;
}

return undefined;
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,13 @@
},
"rule": {
"type": "string"
},
"fixable": {
"type": "boolean"
},
"unsavedOnly": {
"type": "boolean",
"description": "Only apply this customization when the file has unsaved changes."
}
},
"type": "object"
Expand All @@ -529,6 +536,13 @@
},
"rule": {
"type": "string"
},
"fixable": {
"type": "boolean"
},
"unsavedOnly": {
"type": "boolean",
"description": "Only apply this customization when the file has unsaved changes."
}
},
"type": "object"
Expand Down
21 changes: 12 additions & 9 deletions server/src/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,9 @@ export namespace RuleSeverities {

const ruleSeverityCache = new LRUCache<string, RuleSeverity | null>(1024);

export function getOverride(ruleId: string, customizations: RuleCustomization[], isFixable?: boolean): RuleSeverity | undefined {
let result: RuleSeverity | undefined | null = ruleSeverityCache.get(ruleId);
export function getOverride(ruleId: string, customizations: RuleCustomization[], isFixable?: boolean, isDirty?: boolean): RuleSeverity | undefined {
const cacheKey = `${ruleId}:${isDirty ? '1' : '0'}`;
let result: RuleSeverity | undefined | null = ruleSeverityCache.get(cacheKey);
if (result === null) {
return undefined;
}
Expand All @@ -565,17 +566,19 @@ export namespace RuleSeverities {
// Rule name should match
asteriskMatches(customization.rule, ruleId) &&
// Fixable flag should match the fixability of the rule if it's defined
(customization.fixable === undefined || customization.fixable === isFixable)
(customization.fixable === undefined || customization.fixable === isFixable) &&
// unsavedOnly customizations only apply when the document has unsaved changes
(customization.unsavedOnly !== true || isDirty === true)
) {
result = customization.severity;
}
}
if (result === undefined) {
ruleSeverityCache.set(ruleId, null);
ruleSeverityCache.set(cacheKey, null);
return undefined;
}

ruleSeverityCache.set(ruleId, result);
ruleSeverityCache.set(cacheKey, result);
return result;
}

Expand Down Expand Up @@ -625,7 +628,7 @@ export namespace Diagnostics {
return `[${range.start.line},${range.start.character},${range.end.line},${range.end.character}]-${diagnostic.code}-${message ?? ''}`;
}

export function create(settings: TextDocumentSettings, problem: ESLintProblem, document: TextDocument): [Diagnostic, RuleSeverity | undefined] {
export function create(settings: TextDocumentSettings, problem: ESLintProblem, document: TextDocument, isDirty?: boolean): [Diagnostic, RuleSeverity | undefined] {
const message = problem.message;
const startLine = typeof problem.line !== 'number' || Number.isNaN(problem.line) ? 0 : Math.max(0, problem.line - 1);
const startChar = typeof problem.column !== 'number' || Number.isNaN(problem.column) ? 0 : Math.max(0, problem.column - 1);
Expand All @@ -646,7 +649,7 @@ export namespace Diagnostics {
endChar = startLineText.length;
}

const override = RuleSeverities.getOverride(problem.ruleId, settings.rulesCustomizations, problem.fix !== undefined);
const override = RuleSeverities.getOverride(problem.ruleId, settings.rulesCustomizations, problem.fix !== undefined, isDirty);
const result: Diagnostic = {
message: message,
severity: convertSeverityToDiagnosticWithOverride(problem.severity, override),
Expand Down Expand Up @@ -1215,7 +1218,7 @@ export namespace ESLint {
}

const validFixTypes = new Set<string>(['problem', 'suggestion', 'layout', 'directive']);
export async function validate(document: TextDocument, settings: TextDocumentSettings & { library: ESLintModule }): Promise<Diagnostic[]> {
export async function validate(document: TextDocument, settings: TextDocumentSettings & { library: ESLintModule }, isDirty?: boolean): Promise<Diagnostic[]> {
const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options);
let fixTypes: Set<string> | undefined = undefined;
if (Array.isArray(newOptions.fixTypes) && newOptions.fixTypes.length > 0) {
Expand Down Expand Up @@ -1244,7 +1247,7 @@ export namespace ESLint {
if (docReport.messages && Array.isArray(docReport.messages)) {
docReport.messages.forEach((problem) => {
if (problem) {
const [diagnostic, override] = Diagnostics.create(settings, problem, document);
const [diagnostic, override] = Diagnostics.create(settings, problem, document, isDirty);
if (!(override === RuleSeverity.off || (settings.quiet && (diagnostic.severity === DiagnosticSeverity.Warning || diagnostic.severity === DiagnosticSeverity.Information)))) {
diagnostics.push(diagnostic);
}
Expand Down
29 changes: 28 additions & 1 deletion server/src/eslintServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,35 @@ function inferFilePath(documentOrUri: string | TextDocument | URI | undefined, u
ESLint.initialize(connection, documents, inferFilePath, loadNodeModule);
SaveRuleConfigs.inferFilePath = inferFilePath;

// Track the document version at the time it was last opened or saved.
// A document is considered dirty when its current version exceeds the saved version.
// Using versions avoids the false-dirty state caused by onDidChangeContent firing
// on document open (it fires for both open and change notifications).
const savedDocumentVersions = new Map<string, number>();

documents.onDidOpen((event) => {
savedDocumentVersions.set(event.document.uri, event.document.version);
});

documents.onDidSave((event) => {
savedDocumentVersions.set(event.document.uri, event.document.version);
// Force a diagnostics re-pull so that unsavedOnly customizations are re-evaluated
// now that the document is no longer dirty. Without this, the pull diagnostic model
// won't re-request diagnostics after a save when the in-memory content is unchanged.
connection.languages.diagnostics.refresh().catch(() => {
connection.console.error('Failed to refresh diagnostics after save');
});
});

function isDocumentDirty(document: { uri: string; version: number }): boolean {
const savedVersion = savedDocumentVersions.get(document.uri);
return savedVersion !== undefined && document.version > savedVersion;
}

documents.onDidClose(async (event) => {
const document = event.document;
const uri = document.uri;
savedDocumentVersions.delete(uri);
ESLint.removeSettings(uri);
SaveRuleConfigs.remove(uri);
CodeActions.remove(uri);
Expand Down Expand Up @@ -260,7 +286,8 @@ connection.languages.diagnostics.on(async (params) => {
}
try {
const start = Date.now();
const diagnostics = await ESLint.validate(document, settings);
const isDirty = isDocumentDirty(document);
const diagnostics = await ESLint.validate(document, settings, isDirty);
const timeTaken = Date.now() - start;
void connection.sendNotification(StatusNotification.type, { uri: document.uri, state: Status.ok, validationTime: timeTaken });
return {
Expand Down