Skip to content

Commit

Permalink
fix(codefix): use the Aurelia attribute map to validate case of attri…
Browse files Browse the repository at this point in the history
…butes, resolves #54
  • Loading branch information
Erik Lieben authored Dec 29, 2017
1 parent 0921158 commit b143ce8
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 180 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
"dependencies": {
"aurelia-cli": "^0.29.0",
"aurelia-dependency-injection": "^1.3.0",
"aurelia-templating-binding": "^1.4.0",
"parse5": "^3.0.1",
"reflect-metadata": "^0.1.9",
"vscode-languageclient": "^3.5.0",
Expand Down
96 changes: 48 additions & 48 deletions src/client/htmlInvalidCasingCodeActionProvider.ts
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 src/server/aurelia-languageservice/services/htmlValidation.ts
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;
}
}
46 changes: 46 additions & 0 deletions src/shared/attributeInvalidCaseFix.ts
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
};
}
Loading

0 comments on commit b143ce8

Please sign in to comment.