Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change double-quote hygiene rule to tslint rule #6514

Merged
merged 2 commits into from Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 25 additions & 15 deletions build/gulpfile.hygiene.js
Expand Up @@ -289,19 +289,6 @@ function hygiene(some) {

this.emit('data', file);
});

const localizeDoubleQuotes = es.through(function (file) {
const lines = file.__lines;
lines.forEach((line, i) => {
if (/localize\(['"].*['"],\s'.*'\)/.test(line)) {
console.error(file.relative + '(' + (i + 1) + ',1): Message parameter to localize calls should be double-quotes');
errorCount++;
}
});

this.emit('data', file);
});

// {{SQL CARBON EDIT}} END

const formatting = es.map(function (file, cb) {
Expand Down Expand Up @@ -352,6 +339,17 @@ function hygiene(some) {
input = some;
}

const tslintSqlConfiguration = tslint.Configuration.findConfiguration('tslint-sql.json', '.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Youy don't need to add this to the hygiene run, this will automatically run in the tslint check if you have the rule in the tslint directory and have the rule in the tslint file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If your goal is to limit the scope of the files included for this rule, I would just modify the tslint gulp task to run a second pass with just our lint file, rather than putting this into the hygiene task.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also see if vscode has interest in taking this rule, since they have the guidance and when they change code they general update any code that doesn't follow the guidance, they probably just don't see this as big enough of a problem to dedicate time to fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I didn't add it to the tslint.json because that runs on all files and I didn't want to fix up the vscode ones since that'd just result in a bunch of changes that might cause issues merging down the line. Hence the creation of the new tslint file that will run on just the sql code. (I'll update it later to run on our extensions too - I just didn't want to do the changes now since the fix logic isn't working for some reason so I need to investigate that further and doing the replacements manually is a pain)

And I still want it to run as part of hygiene so that the precommit hook catches it - since my current understanding if I'm reading the configuration correctly is that it only runs the hygiene task as a precommit hook and thus if someone ignores the GCI failing (which people do quite a bit) they might not catch the issue.

I'll follow up with seeing if vscode will take this but for now I figure we can get it in our repo and then remove it later if they decide to take it.

const tslintSqlOptions = { fix: false, formatter: 'json' };
const sqlTsLinter = new tslint.Linter(tslintSqlOptions);

const sqlTsl = es.through(function (file) {
const contents = file.contents.toString('utf8');
sqlTsLinter.lint(file.relative, contents, tslintSqlConfiguration.results);

this.emit('data', file);
});

const productJsonFilter = filter('product.json', { restore: true });

const result = input
Expand All @@ -371,9 +369,8 @@ function hygiene(some) {
// {{SQL CARBON EDIT}}
.pipe(filter(useStrictFilter))
.pipe(useStrict)
// Only look at files under the sql folder since we don't want to cause conflicts with VS code
.pipe(filter(sqlFilter))
.pipe(localizeDoubleQuotes);
.pipe(sqlTsl);

const javascript = result
.pipe(filter(eslintFilter))
Expand Down Expand Up @@ -405,6 +402,19 @@ function hygiene(some) {
errorCount += tslintResult.failures.length;
}

const sqlTslintResult = sqlTsLinter.getResult();
if (sqlTslintResult.failures.length > 0) {
for (const failure of sqlTslintResult.failures) {
const name = failure.getFileName();
const position = failure.getStartPosition();
const line = position.getLineAndCharacter().line;
const character = position.getLineAndCharacter().character;

console.error(`${name}:${line + 1}:${character + 1}:${failure.getFailure()}`);
}
errorCount += sqlTslintResult.failures.length;
}

if (errorCount > 0) {
this.emit('error', 'Hygiene failed with ' + errorCount + ' errors. Check \'build/gulpfile.hygiene.js\'.');
} else {
Expand Down
60 changes: 60 additions & 0 deletions build/lib/tslint/doubleQuotedStringArgRule.js
@@ -0,0 +1,60 @@
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
const ts = require("typescript");
const Lint = require("tslint");
/**
* Implementation of the double-quoted-string-arg rule which verifies that the specified index of calls matching
* the specified signatures is quoted with double-quotes only.
*/
class Rule extends Lint.Rules.AbstractRule {
apply(sourceFile) {
return this.applyWithWalker(new DoubleQuotedStringArgRuleWalker(sourceFile, this.getOptions()));
}
}
exports.Rule = Rule;
class DoubleQuotedStringArgRuleWalker extends Lint.RuleWalker {
constructor(file, opts) {
super(file, opts);
this.signatures = Object.create(null);
this.argIndex = undefined;
const options = this.getOptions();
const first = options && options.length > 0 ? options[0] : null;
if (first) {
if (Array.isArray(first.signatures)) {
first.signatures.forEach((signature) => this.signatures[signature] = true);
}
if (typeof first.argIndex !== 'undefined') {
this.argIndex = first.argIndex;
}
}
}
visitCallExpression(node) {
this.checkCallExpression(node);
super.visitCallExpression(node);
}
checkCallExpression(node) {
// Not one of the functions we're looking for, continue on
const functionName = node.expression.getText();
if (functionName && !this.signatures[functionName]) {
return;
}
const arg = node.arguments[this.argIndex];
// Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue
if (arg && ts.isStringLiteral(arg)) {
const argText = arg.getText();
const doubleQuotedArg = argText.length >= 2 && argText[0] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE && argText[argText.length - 1] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE;
if (!doubleQuotedArg) {
const fix = [
Lint.Replacement.replaceFromTo(arg.getStart(), arg.getWidth(), `"${arg.getText().slice(1, arg.getWidth() - 2)}"`),
];
this.addFailure(this.createFailure(arg.getStart(), arg.getWidth(), `Argument ${this.argIndex + 1} to '${functionName}' must be double quoted.`, fix));
return;
}
}
}
}
DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE = '"';
82 changes: 82 additions & 0 deletions build/lib/tslint/doubleQuotedStringArgRule.ts
@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as ts from 'typescript';
import * as Lint from 'tslint';

/**
* Implementation of the double-quoted-string-arg rule which verifies that the specified index of calls matching
* the specified signatures is quoted with double-quotes only.
*/
export class Rule extends Lint.Rules.AbstractRule {
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new DoubleQuotedStringArgRuleWalker(sourceFile, this.getOptions()));
}
}

interface Map<V> {
[key: string]: V;
}

interface DoubleQuotedStringArgOptions {
signatures?: string[];
argIndex?: number;
}

class DoubleQuotedStringArgRuleWalker extends Lint.RuleWalker {

private static DOUBLE_QUOTE: string = '"';

private signatures: Map<boolean>;
private argIndex: number | undefined;

constructor(file: ts.SourceFile, opts: Lint.IOptions) {
super(file, opts);
this.signatures = Object.create(null);
this.argIndex = undefined;
const options: any[] = this.getOptions();
const first: DoubleQuotedStringArgOptions = options && options.length > 0 ? options[0] : null;
if (first) {
if (Array.isArray(first.signatures)) {
first.signatures.forEach((signature: string) => this.signatures[signature] = true);
}
if (typeof first.argIndex !== 'undefined') {
this.argIndex = first.argIndex;
}
}
}

protected visitCallExpression(node: ts.CallExpression): void {
this.checkCallExpression(node);
super.visitCallExpression(node);
}

private checkCallExpression(node: ts.CallExpression): void {
// Not one of the functions we're looking for, continue on
const functionName = node.expression.getText();
if (functionName && !this.signatures[functionName]) {
return;
}

const arg = node.arguments[this.argIndex!];

// Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue
if(arg && ts.isStringLiteral(arg)) {
const argText = arg.getText();
const doubleQuotedArg = argText.length >= 2 && argText[0] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE && argText[argText.length - 1] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE;

if (!doubleQuotedArg) {
const fix = [
Lint.Replacement.replaceFromTo(arg.getStart(), arg.getWidth(), `"${arg.getText().slice(1, arg.getWidth() - 2)}"`),
];
this.addFailure(this.createFailure(
arg.getStart(), arg.getWidth(),
`Argument ${this.argIndex! + 1} to '${functionName}' must be double quoted.`, fix));
return;
}
}

}
}
4 changes: 2 additions & 2 deletions src/sql/platform/query/common/queryModelService.ts
Expand Up @@ -254,7 +254,7 @@ export class QueryModelService implements IQueryModelService {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
messageText = nls.localize('runQueryStringBatchStartMessage', "Started executing query \"{0}\"", info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, b.selection.startLine + 1)
Expand Down Expand Up @@ -405,7 +405,7 @@ export class QueryModelService implements IQueryModelService {
if (info.selectionSnippet) {
// This indicates it's a query string. Do not include line information since it'll be inaccurate, but show some of the
// executed query text
messageText = nls.localize('runQueryStringBatchStartMessage', 'Started executing query "{0}"', info.selectionSnippet);
messageText = nls.localize('runQueryStringBatchStartMessage', "Started executing query \"{0}\"", info.selectionSnippet);
} else {
link = {
text: strings.format(LocalizedConstants.runQueryBatchStartLine, batch.selection.startLine + 1)
Expand Down
8 changes: 4 additions & 4 deletions src/sql/platform/query/common/queryRunner.ts
Expand Up @@ -218,7 +218,7 @@ export default class QueryRunner extends Disposable {
if (error instanceof Error) {
error = error.message;
}
let message = nls.localize('query.ExecutionFailedError', 'Execution failed due to an unexpected error: {0}\t{1}', eol, error);
let message = nls.localize('query.ExecutionFailedError', "Execution failed due to an unexpected error: {0}\t{1}", eol, error);
this.handleMessage(<azdata.QueryExecuteMessageParams>{
ownerUri: this.uri,
message: {
Expand Down Expand Up @@ -252,7 +252,7 @@ export default class QueryRunner extends Disposable {
// We're done with this query so shut down any waiting mechanisms

let message = {
message: nls.localize('query.message.executionTime', 'Total execution time: {0}', timeStamp),
message: nls.localize('query.message.executionTime', "Total execution time: {0}", timeStamp),
isError: false,
time: undefined
};
Expand Down Expand Up @@ -282,7 +282,7 @@ export default class QueryRunner extends Disposable {

let message = {
// account for index by 1
message: nls.localize('query.message.startQuery', 'Started executing query at Line {0}', batch.selection.startLine + 1),
message: nls.localize('query.message.startQuery', "Started executing query at Line {0}", batch.selection.startLine + 1),
time: new Date(batch.executionStart).toLocaleTimeString(),
selection: batch.selection,
isError: false
Expand Down Expand Up @@ -563,7 +563,7 @@ export default class QueryRunner extends Disposable {
if (showBatchTime) {
let message: IQueryMessage = {
batchId: batchId,
message: nls.localize('elapsedBatchTime', 'Batch execution time: {0}', executionTime),
message: nls.localize('elapsedBatchTime', "Batch execution time: {0}", executionTime),
time: undefined,
isError: false
};
Expand Down
2 changes: 1 addition & 1 deletion src/sql/workbench/api/common/extHostModelViewTree.ts
Expand Up @@ -45,7 +45,7 @@ export class ExtHostModelViewTreeViews implements ExtHostModelViewTreeViewsShape
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {

return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId)));
return Promise.reject(new Error(localize('treeView.notRegistered', "No tree view with id \'{0}\' registered.", treeViewId)));
}
return treeView.getChildren(treeItemHandle);
}
Expand Down
6 changes: 3 additions & 3 deletions src/sql/workbench/api/common/extHostNotebook.ts
Expand Up @@ -289,7 +289,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
return this._withNotebookManager(handle, (notebookManager) => {
let serverManager = notebookManager.serverManager;
if (!serverManager) {
return Promise.reject(new Error(localize('noServerManager', 'Notebook Manager for notebook {0} does not have a server manager. Cannot perform operations on it', notebookManager.uriString)));
return Promise.reject(new Error(localize('noServerManager', "Notebook Manager for notebook {0} does not have a server manager. Cannot perform operations on it", notebookManager.uriString)));
}
return callback(serverManager);
});
Expand All @@ -299,7 +299,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
return this._withNotebookManager(handle, (notebookManager) => {
let contentManager = notebookManager.contentManager;
if (!contentManager) {
return Promise.reject(new Error(localize('noContentManager', 'Notebook Manager for notebook {0} does not have a content manager. Cannot perform operations on it', notebookManager.uriString)));
return Promise.reject(new Error(localize('noContentManager', "Notebook Manager for notebook {0} does not have a content manager. Cannot perform operations on it", notebookManager.uriString)));
}
return callback(contentManager);
});
Expand All @@ -309,7 +309,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
return this._withNotebookManager(handle, (notebookManager) => {
let sessionManager = notebookManager.sessionManager;
if (!sessionManager) {
return Promise.reject(new Error(localize('noSessionManager', 'Notebook Manager for notebook {0} does not have a session manager. Cannot perform operations on it', notebookManager.uriString)));
return Promise.reject(new Error(localize('noSessionManager', "Notebook Manager for notebook {0} does not have a session manager. Cannot perform operations on it", notebookManager.uriString)));
}
return callback(sessionManager);
});
Expand Down
4 changes: 2 additions & 2 deletions src/sql/workbench/electron-browser/scriptingUtils.ts
Expand Up @@ -164,7 +164,7 @@ export function script(connectionProfile: IConnectionProfile, metadata: azdata.O
reject(editorError);
});
} else {
let scriptNotFoundMsg = nls.localize('scriptNotFoundForObject', 'No script was returned when scripting as {0} on object {1}',
let scriptNotFoundMsg = nls.localize('scriptNotFoundForObject', "No script was returned when scripting as {0} on object {1}",
GetScriptOperationName(operation), metadata.metadataTypeName);
let messageDetail = '';
let operationResult = scriptingService.getOperationFailedResult(result.operationId);
Expand All @@ -179,7 +179,7 @@ export function script(connectionProfile: IConnectionProfile, metadata: azdata.O
reject(scriptNotFoundMsg);
}
} else {
reject(nls.localize('scriptNotFound', 'No script was returned when scripting as {0}', GetScriptOperationName(operation)));
reject(nls.localize('scriptNotFound', "No script was returned when scripting as {0}", GetScriptOperationName(operation)));
}
}, scriptingError => {
reject(scriptingError);
Expand Down
2 changes: 1 addition & 1 deletion src/sql/workbench/parts/charts/browser/actions.ts
Expand Up @@ -169,7 +169,7 @@ export class SaveImageAction extends Action {
this.windowsService.openExternal(filePath.toString());
this.notificationService.notify({
severity: Severity.Error,
message: localize('chartSaved', 'Saved Chart to path: {0}', filePath.toString())
message: localize('chartSaved', "Saved Chart to path: {0}", filePath.toString())
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/sql/workbench/parts/charts/browser/graphInsight.ts
Expand Up @@ -128,7 +128,7 @@ export class Graph implements IInsight {
chartData = data.rows.map((row, i) => {
return {
data: row.map(item => Number(item)),
label: localize('series', 'Series {0}', i)
label: localize('series', "Series {0}", i)
};
});
}
Expand All @@ -144,7 +144,7 @@ export class Graph implements IInsight {
chartData = data.rows[0].slice(1).map((row, i) => {
return {
data: data.rows.map(row => Number(row[i + 1])),
label: localize('series', 'Series {0}', i + 1)
label: localize('series', "Series {0}", i + 1)
};
});
}
Expand Down
Expand Up @@ -92,7 +92,7 @@ export class CommandLineWorkbenchContribution implements IWorkbenchContribution
let connectedContext: azdata.ConnectedContext = undefined;
if (profile) {
if (this._notificationService) {
this._notificationService.status(localize('connectingLabel', 'Connecting: {0}', profile.serverName), { hideAfter: 2500 });
this._notificationService.status(localize('connectingLabel', "Connecting: {0}", profile.serverName), { hideAfter: 2500 });
}
try {
await this._connectionManagementService.connectIfNotConnected(profile, 'connection', true);
Expand All @@ -106,7 +106,7 @@ export class CommandLineWorkbenchContribution implements IWorkbenchContribution
}
if (commandName) {
if (this._notificationService) {
this._notificationService.status(localize('runningCommandLabel', 'Running command: {0}', commandName), { hideAfter: 2500 });
this._notificationService.status(localize('runningCommandLabel', "Running command: {0}", commandName), { hideAfter: 2500 });
}
await this._commandService.executeCommand(commandName, connectedContext);
} else if (profile) {
Expand All @@ -119,7 +119,7 @@ export class CommandLineWorkbenchContribution implements IWorkbenchContribution
else {
// Default to showing new query
if (this._notificationService) {
this._notificationService.status(localize('openingNewQueryLabel', 'Opening new query: {0}', profile.serverName), { hideAfter: 2500 });
this._notificationService.status(localize('openingNewQueryLabel', "Opening new query: {0}", profile.serverName), { hideAfter: 2500 });
}
try {
await TaskUtilities.newQuery(profile,
Expand Down
Expand Up @@ -40,7 +40,7 @@ export class DashboardErrorContainer extends DashboardTab implements AfterViewIn

ngAfterViewInit() {
const errorMessage = this._errorMessageContainer.nativeElement as HTMLElement;
errorMessage.innerText = nls.localize('dashboardNavSection_loadTabError', 'The "{0}" section has invalid content. Please contact extension owner.', this.tab.title);
errorMessage.innerText = nls.localize('dashboardNavSection_loadTabError', "The \"{0}\" section has invalid content. Please contact extension owner.", this.tab.title);
}

public get id(): string {
Expand Down
Expand Up @@ -179,7 +179,7 @@ export function getDashboardContainer(container: object, logService: ILogService
if (!containerTypeFound) {
const dashboardContainer = dashboardcontainerRegistry.getRegisteredContainer(key);
if (!dashboardContainer) {
const errorMessage = nls.localize('unknownDashboardContainerError', '{0} is an unknown container.', key);
const errorMessage = nls.localize('unknownDashboardContainerError', "{0} is an unknown container.", key);
logService.error(errorMessage);
return { result: false, message: errorMessage, container: undefined };
} else {
Expand Down