Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Commit

Permalink
Add 'encoding' rule
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-hanson committed Mar 18, 2017
1 parent 2dad217 commit e52fb9c
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 7 deletions.
120 changes: 120 additions & 0 deletions src/rules/encodingRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @license
* Copyright 2017 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as fs from "fs";
import * as ts from "typescript";

import * as Lint from "../index";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "encoding",
description: "Enforces UTF-8 file encoding.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "style",
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING(actual: Encoding): string {
return `This file is encoded as ${showEncoding(actual)} instead of UTF-8.`;
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}

function walk(ctx: Lint.WalkContext<void>): void {
const encoding = detectEncoding(ctx.sourceFile.fileName);
if (encoding !== "utf8") {
ctx.addFailure(0, 1, Rule.FAILURE_STRING(encoding));
}
}

function showEncoding(encoding: Encoding): string {
switch (encoding) {
case "utf8":
return "UTF-8";
case "utf8-bom":
return "UTF-8 with byte-order marker (BOM)";
case "utf16le":
return "UTF-16 (little-endian)";
case "utf16be":
return "UTF-16 (big-endian)";
}
}

export type Encoding = "utf8" | "utf8-bom" | "utf16le" | "utf16be";
function detectEncoding(fileName: string): Encoding {
const fd = fs.openSync(fileName, "r");
const maxBytesRead = 3; // Only need 3 bytes to detect the encoding.
const buffer = new Buffer(maxBytesRead);
const bytesRead = fs.readSync(fd, buffer, /*offset*/ 0, /*length*/ maxBytesRead, /*position*/ 0);
return detectBufferEncoding(buffer, bytesRead);
}

export function readBufferWithDetectedEncoding(buffer: Buffer): string {
switch (detectBufferEncoding(buffer)) {
case "utf8":
return buffer.toString();
case "utf8-bom":
return buffer.toString("utf-8", 2);
case "utf16le":
return buffer.toString("utf16le", 2);
case "utf16be":
// Round down to nearest multiple of 2.
const len = buffer.length & ~1; // tslint:disable-line no-bitwise
// Flip all byte pairs, then read as little-endian.
for (let i = 0; i < len; i += 2) {
const temp = buffer[i];
buffer[i] = buffer[i + 1];
buffer[i + 1] = temp;
}
return buffer.toString("utf16le", 2);
}
}

function detectBufferEncoding(buffer: Buffer, length = buffer.length): Encoding {
if (length < 2) {
return "utf8";
}

switch (buffer[0]) {
case 0xef:
if (buffer[1] === 0xbb && length >= 3 && buffer[2] === 0xbf) {
return "utf8-bom";
}
break;

case 0xfe:
if (buffer[1] === 0xff) {
return "utf16be";
}
break;

case 0xff:
if (buffer[1] === 0xfe) {
return "utf16le";
}
}

return "utf8";
}
14 changes: 7 additions & 7 deletions src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as ts from "typescript";

import {Fix} from "./language/rule/rule";
import * as Linter from "./linter";
import {readBufferWithDetectedEncoding} from "./rules/encodingRule";
import {LintError} from "./test/lintError";
import * as parse from "./test/parse";

Expand Down Expand Up @@ -86,9 +87,8 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[
const results: TestResult = { directory: testDirectory, results: {} };

for (const fileToLint of filesToLint) {
const fileBasename = path.basename(fileToLint, MARKUP_FILE_EXTENSION);
const fileCompileName = fileBasename.replace(/\.lint$/, "");
let fileText = fs.readFileSync(fileToLint, "utf8");
const fileCompileName = path.basename(fileToLint, MARKUP_FILE_EXTENSION);
let fileText = readBufferWithDetectedEncoding(fs.readFileSync(fileToLint));
const tsVersionRequirement = parse.getTypescriptVersionRequirement(fileText);
if (tsVersionRequirement) {
const tsVersion = new semver.SemVer(ts.version);
Expand Down Expand Up @@ -123,12 +123,12 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[
getSourceFile(filenameToGet: string) {
const target = compilerOptions.target === undefined ? ts.ScriptTarget.ES5 : compilerOptions.target;
if (filenameToGet === ts.getDefaultLibFileName(compilerOptions)) {
const fileContent = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions)).toString();
const fileContent = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions), "utf-8");
return ts.createSourceFile(filenameToGet, fileContent, target);
} else if (filenameToGet === fileCompileName) {
return ts.createSourceFile(fileBasename, fileTextWithoutMarkup, target, true);
return ts.createSourceFile(fileToLint, fileTextWithoutMarkup, target, true);
} else if (fs.existsSync(path.resolve(path.dirname(fileToLint), filenameToGet))) {
const text = fs.readFileSync(path.resolve(path.dirname(fileToLint), filenameToGet), {encoding: "utf-8"});
const text = fs.readFileSync(path.resolve(path.dirname(fileToLint), filenameToGet), "utf-8");
return ts.createSourceFile(filenameToGet, text, target, true);
}
throw new Error(`Couldn't get source file '${filenameToGet}'`);
Expand All @@ -150,7 +150,7 @@ export function runTest(testDirectory: string, rulesDirectory?: string | string[
rulesDirectory,
};
const linter = new Linter(lintOptions, program);
linter.lint(fileBasename, fileTextWithoutMarkup, tslintConfig);
linter.lint(fileToLint, fileTextWithoutMarkup, tslintConfig);
const failures = linter.getResult().failures;
const errorsFromLinter: LintError[] = failures.map((failure) => {
const startLineAndCharacter = failure.getStartPosition().getLineAndCharacter();
Expand Down
8 changes: 8 additions & 0 deletions test/rules/encoding/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"jsRules": {
"encoding": true
},
"rules": {
"encoding": true
}
}
Binary file added test/rules/encoding/utf16be.js.lint
Binary file not shown.
Binary file added test/rules/encoding/utf16le.js.lint
Binary file not shown.
2 changes: 2 additions & 0 deletions test/rules/encoding/utf8-bom.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
A
~ [This file is encoded as UTF-8 with byte-order marker (BOM) instead of UTF-8.]
Empty file.

0 comments on commit e52fb9c

Please sign in to comment.