-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(codefix): use the Aurelia attribute map to validate case of attri…
…butes, resolves #54
- Loading branch information
Erik Lieben
authored
Dec 29, 2017
1 parent
0921158
commit b143ce8
Showing
5 changed files
with
219 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,48 @@ | ||
'use strict'; | ||
import vscode = require('vscode'); | ||
|
||
export default class HtmlInvalidCasingActionProvider implements vscode.CodeActionProvider { | ||
private static commandId: string = 'aurelia-fix-invalid-casing'; | ||
private command: vscode.Disposable; | ||
|
||
public activate(subscriptions: vscode.Disposable[]) { | ||
this.command = vscode.commands.registerCommand(HtmlInvalidCasingActionProvider.commandId, this.fixInvalidCasing, this); | ||
subscriptions.push(this); | ||
} | ||
|
||
public provideCodeActions( | ||
document: vscode.TextDocument, | ||
range: vscode.Range, | ||
context: vscode.CodeActionContext, | ||
token: vscode.CancellationToken): vscode.Command[] { | ||
|
||
let diagnostic: vscode.Diagnostic = context.diagnostics[0]; | ||
let text = document.getText(diagnostic.range); | ||
const kebabCaseValidationRegex = /(.*)\.(bind|one-way|two-way|one-time|call|delegate|trigger)/; | ||
|
||
let result = kebabCaseValidationRegex.exec(text); | ||
let attribute = result[1]; | ||
let binding = result[2]; | ||
let fixedAttribute = attribute.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join('-'); | ||
let fixedText = `${fixedAttribute}.${binding}`; | ||
let commands: vscode.Command[] = []; | ||
|
||
commands.push({ | ||
arguments: [document, diagnostic.range, fixedText], | ||
command: HtmlInvalidCasingActionProvider.commandId, | ||
title: `Rename ${attribute} to ${fixedAttribute}`, | ||
}); | ||
|
||
return commands; | ||
} | ||
|
||
public fixInvalidCasing(document, range, fixedText) { | ||
let edit = new vscode.WorkspaceEdit(); | ||
edit.replace(document.uri, range, fixedText); | ||
return vscode.workspace.applyEdit(edit); | ||
} | ||
|
||
public dispose(): void { | ||
this.command.dispose(); | ||
} | ||
} | ||
'use strict'; | ||
import vscode = require('vscode'); | ||
import { attributeInvalidCaseFix } from './../shared/attributeInvalidCaseFix'; | ||
|
||
export default class HtmlInvalidCasingActionProvider implements vscode.CodeActionProvider { | ||
private static commandId: string = 'aurelia-fix-invalid-casing'; | ||
private command: vscode.Disposable; | ||
|
||
public activate(subscriptions: vscode.Disposable[]) { | ||
this.command = vscode.commands.registerCommand(HtmlInvalidCasingActionProvider.commandId, this.fixInvalidCasing, this); | ||
subscriptions.push(this); | ||
} | ||
|
||
public provideCodeActions( | ||
document: vscode.TextDocument, | ||
range: vscode.Range, | ||
context: vscode.CodeActionContext, | ||
token: vscode.CancellationToken): vscode.Command[] { | ||
|
||
const diagnostic: vscode.Diagnostic = context.diagnostics[0]; | ||
const text = document.getText(diagnostic.range); | ||
|
||
const elementResult = /<([a-zA-Z\-]*) .*$/g.exec(document.getText(diagnostic.range.with(new vscode.Position(0,0)))); | ||
const elementName = elementResult[1]; | ||
|
||
const {attribute, command, fixed} = attributeInvalidCaseFix(elementName, text); | ||
const fixedText = `${fixed}.${command}`; | ||
const commands: vscode.Command[] = []; | ||
|
||
commands.push({ | ||
arguments: [document, diagnostic.range, fixedText], | ||
command: HtmlInvalidCasingActionProvider.commandId, | ||
title: `Rename ${attribute} to ${fixed}`, | ||
}); | ||
|
||
return commands; | ||
} | ||
|
||
public fixInvalidCasing(document, range, fixedText) { | ||
let edit = new vscode.WorkspaceEdit(); | ||
edit.replace(document.uri, range, fixedText); | ||
return vscode.workspace.applyEdit(edit); | ||
} | ||
|
||
public dispose(): void { | ||
this.command.dispose(); | ||
} | ||
} |
219 changes: 105 additions & 114 deletions
219
src/server/aurelia-languageservice/services/htmlValidation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,114 +1,105 @@ | ||
'use strict'; | ||
import { Diagnostic, DiagnosticSeverity, Range, TextDocument } from 'vscode-languageserver-types'; | ||
import { LanguageSettings } from '../aureliaLanguageService'; | ||
import { HTMLDocument } from '../parser/htmlParser'; | ||
import { TokenType, createScanner } from '../parser/htmlScanner'; | ||
|
||
export type DiagnosticCodes = 'invalid-casing' | 'invalid-method'; | ||
export const DiagnosticCodes = { | ||
InvalidCasing: 'invalid-casing' as DiagnosticCodes, | ||
InvalidMethod: 'invalid-method' as DiagnosticCodes, | ||
}; | ||
|
||
export const DiagnosticSource = 'Aurelia'; | ||
|
||
interface Attribute { | ||
name: string; | ||
start: number; | ||
length: number; | ||
value?: string; | ||
} | ||
|
||
const kebabCaseValidationRegex = /(.*)\.(bind|one-way|two-way|one-time|call|delegate|trigger)/; | ||
|
||
function camelToKebab(s: string) { | ||
return s.replace(/\.?([A-Z])/g, (x, y) => '-' + y.toLowerCase()).replace(/^-/, ''); | ||
} | ||
|
||
export class HTMLValidation { | ||
private validationEnabled: boolean; | ||
|
||
constructor() { | ||
this.validationEnabled = true; | ||
} | ||
|
||
public configure(raw: LanguageSettings) { | ||
if (raw) { | ||
this.validationEnabled = raw.validate; | ||
} | ||
} | ||
|
||
public async doValidation(document: TextDocument, htmlDocument: HTMLDocument): Promise<Diagnostic[]> { | ||
if (!this.validationEnabled) { | ||
return Promise.resolve([]); | ||
} | ||
|
||
// handle empty document cases | ||
if (!htmlDocument || !htmlDocument.roots || htmlDocument.roots.length === 0) { | ||
return Promise.resolve([]); | ||
} | ||
|
||
const text = document.getText(); | ||
const scanner = createScanner(text, htmlDocument.roots[0].start); | ||
|
||
const diagnostics: Diagnostic[] = []; | ||
|
||
let attr; | ||
let token = scanner.scan(); | ||
while (token !== TokenType.EOS) { | ||
// tslint:disable-next-line:switch-default | ||
switch (token) { | ||
case TokenType.AttributeName: | ||
attr = <Attribute> { | ||
length: scanner.getTokenLength(), | ||
name: scanner.getTokenText(), | ||
start: scanner.getTokenOffset(), | ||
}; | ||
break; | ||
case TokenType.AttributeValue: | ||
attr.value = scanner.getTokenText(); | ||
await this.validateAttribute(attr, document, diagnostics); | ||
break; | ||
} | ||
token = scanner.scan(); | ||
} | ||
|
||
return Promise.resolve(diagnostics); | ||
} | ||
|
||
private async validateAttribute(attr: Attribute, document: TextDocument, diagnostics: Diagnostic[]) { | ||
let match = kebabCaseValidationRegex.exec(attr.name); | ||
if (match && match.length) { | ||
const prop = match[1]; | ||
const op = match[2]; | ||
|
||
if (prop !== prop.toLowerCase()) { | ||
diagnostics.push(this.toDiagnostic(attr, document, | ||
`'${attr.name}' has invalid casing; it should likely be '${camelToKebab(prop)}.${op}'`, | ||
DiagnosticCodes.InvalidCasing)); | ||
} | ||
} | ||
|
||
return Promise.resolve(); | ||
} | ||
|
||
private toDiagnostic( | ||
attr: Attribute, | ||
document: TextDocument, | ||
message: string, | ||
code: DiagnosticCodes | undefined = undefined, | ||
serverity: DiagnosticSeverity = DiagnosticSeverity.Error): Diagnostic { | ||
const range = Range.create(document.positionAt(attr.start), document.positionAt(attr.start + attr.length)); | ||
const diagnostic = <Diagnostic> { | ||
message: message, | ||
range: range, | ||
severity: serverity, | ||
source: DiagnosticSource, | ||
}; | ||
|
||
if (code !== undefined) { | ||
diagnostic.code = code; | ||
} | ||
return diagnostic; | ||
} | ||
} | ||
'use strict'; | ||
import { Diagnostic, DiagnosticSeverity, Range, TextDocument } from 'vscode-languageserver-types'; | ||
import { LanguageSettings } from '../aureliaLanguageService'; | ||
import { HTMLDocument } from '../parser/htmlParser'; | ||
import { TokenType, createScanner } from '../parser/htmlScanner'; | ||
import { attributeInvalidCaseFix } from './../../../shared/attributeInvalidCaseFix'; | ||
|
||
export type DiagnosticCodes = 'invalid-casing' | 'invalid-method'; | ||
export const DiagnosticCodes = { | ||
InvalidCasing: 'invalid-casing' as DiagnosticCodes, | ||
InvalidMethod: 'invalid-method' as DiagnosticCodes, | ||
}; | ||
|
||
export const DiagnosticSource = 'Aurelia'; | ||
|
||
interface Attribute { | ||
name: string; | ||
start: number; | ||
length: number; | ||
value?: string; | ||
} | ||
|
||
export class HTMLValidation { | ||
private validationEnabled: boolean; | ||
|
||
constructor() { | ||
this.validationEnabled = true | ||
} | ||
|
||
public configure(raw: LanguageSettings) { | ||
if (raw) { | ||
this.validationEnabled = raw.validate; | ||
} | ||
} | ||
|
||
public async doValidation(document: TextDocument, htmlDocument: HTMLDocument): Promise<Diagnostic[]> { | ||
if (!this.validationEnabled) { | ||
return Promise.resolve([]); | ||
} | ||
|
||
// handle empty document cases | ||
if (!htmlDocument || !htmlDocument.roots || htmlDocument.roots.length === 0) { | ||
return Promise.resolve([]); | ||
} | ||
|
||
const text = document.getText(); | ||
const scanner = createScanner(text, htmlDocument.roots[0].start); | ||
const diagnostics: Diagnostic[] = []; | ||
|
||
let attr; | ||
let token = scanner.scan(); | ||
let elementName = ''; | ||
while (token !== TokenType.EOS) { | ||
// tslint:disable-next-line:switch-default | ||
switch (token) { | ||
case TokenType.AttributeName: | ||
attr = <Attribute> { | ||
length: scanner.getTokenLength(), | ||
name: scanner.getTokenText(), | ||
start: scanner.getTokenOffset(), | ||
}; | ||
break; | ||
case TokenType.AttributeValue: | ||
attr.value = scanner.getTokenText(); | ||
this.validateAttribute(attr, elementName, document, diagnostics); | ||
break; | ||
case TokenType.StartTag: | ||
elementName = scanner.getTokenText(); | ||
break; | ||
} | ||
token = scanner.scan(); | ||
} | ||
|
||
return Promise.resolve(diagnostics); | ||
} | ||
|
||
private validateAttribute(attr: Attribute, elementName: string, document: TextDocument, diagnostics: Diagnostic[]) { | ||
const {attribute, command, fixed} = attributeInvalidCaseFix(elementName, attr.name); | ||
if(fixed && fixed !== attribute) { | ||
diagnostics.push(this.toDiagnostic(attr, document, | ||
`'${attr.name}' has invalid casing; it should likely be '${fixed}.${command}'`, | ||
DiagnosticCodes.InvalidCasing)); | ||
} | ||
} | ||
|
||
private toDiagnostic( | ||
attr: Attribute, | ||
document: TextDocument, | ||
message: string, | ||
code: DiagnosticCodes | undefined = undefined, | ||
serverity: DiagnosticSeverity = DiagnosticSeverity.Error): Diagnostic { | ||
const range = Range.create(document.positionAt(attr.start), document.positionAt(attr.start + attr.length)); | ||
const diagnostic = <Diagnostic> { | ||
message: message, | ||
range: range, | ||
severity: serverity, | ||
source: DiagnosticSource, | ||
}; | ||
|
||
if (code !== undefined) { | ||
diagnostic.code = code; | ||
} | ||
return diagnostic; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { AttributeMap } from 'aurelia-templating-binding'; | ||
|
||
export function attributeInvalidCaseFix(elementName, attributeText) { | ||
|
||
const attributeMap = new AttributeMap({ | ||
isStandardSvgAttribute: () => false | ||
}); | ||
|
||
const kebabCaseValidationRegex = /(.*)\.(bind|one-way|two-way|one-time|from-view|to-view|ref|call|delegate|trigger)/; | ||
const match = kebabCaseValidationRegex.exec(attributeText); | ||
if (match) { | ||
const attribute = match[1]; | ||
const command = match[2]; | ||
|
||
let fixed; | ||
const element = attributeMap.elements[elementName]; | ||
|
||
// only replace dashes in non data-* and aria-* attributes | ||
let lookupProperty = attribute.toLowerCase(); | ||
if (/^(?!(data|aria)-).*$/g.test(lookupProperty)) { | ||
lookupProperty = lookupProperty.replace('-',''); | ||
} | ||
|
||
if (element && lookupProperty in element) { | ||
fixed = element[lookupProperty]; | ||
} | ||
else if (lookupProperty in attributeMap.allElements) { | ||
fixed = attributeMap.allElements[lookupProperty]; | ||
} | ||
else { | ||
fixed = attribute.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join('-'); | ||
} | ||
|
||
return { | ||
attribute, | ||
command, | ||
fixed | ||
}; | ||
} | ||
|
||
return { | ||
attribute: attributeText, | ||
command: undefined, | ||
fixed: undefined | ||
}; | ||
} |
Oops, something went wrong.