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

Added new linting package #306

Closed
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
dist
*.tsbuildinfo
*.lerna_backup
packages/linting/cache
packages/utils/cache
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ A monorepo for formerly disparate DefinitelyTyped-related tools:
- [definitions-parser](packages/definitions-parser): the part of [microsoft/types-publisher](https://github.com/microsoft/types-publisher) that reads DefinitelyTyped repository data
- [dtslint-runner](packages/dtslint-runner): [DefinitelyTyped/dtslint-runner](https://github.com/DefinitelyTyped/dtslint-runner)
- [header-parser](packages/header-parser): [microsoft/definitelytyped-header-parser](https://github.com/microsoft/definitelytyped-header-parser)
- [linting](packages/linting): linting utilities, mostly extracted from [microsoft/dtslint](https://github.com/microsoft/dtslint)
- [perf](packages/perf): [andrewbranch/definitely-not-slow](https://github.com/andrewbranch/definitely-not-slow)
- [publisher](packages/publisher): the rest of [microsoft/types-publisher](https://github.com/microsoft/types-publisher)
- [retag](packages/retag): [DefinitelyTyped/dt-retag](https://github.com/DefinitelyTyped/dt-retag)
Expand Down
1 change: 1 addition & 0 deletions packages/linting/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
logs
7 changes: 7 additions & 0 deletions packages/linting/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
src
test
logs
.DS_Store
.vscode
*.tsbuildinfo
tsconfig.json
3 changes: 3 additions & 0 deletions packages/linting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @definitelytyped/linting

Linting utilities for DefinitelyTyped. Mostly extracted from [microsoft/dtslint](https://github.com/microsoft/dtslint).
43 changes: 43 additions & 0 deletions packages/linting/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@definitelytyped/linting",
"version": "0.0.84",
"description": "Linting utilities for DefinitelyTyped",
"homepage": "https://github.com/microsoft/DefinitelyTyped-tools/tree/master/packages/linting#readme",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/DefinitelyTyped-tools.git",
"directory": "packages/linting"
},
"scripts": {
"build": "tsc -b"
},
"bugs": {
"url": "https://github.com/microsoft/DefinitelyTyped-tools/issues"
},
"dependencies": {
"@definitelytyped/header-parser": "^0.0.84",
"@definitelytyped/typescript-versions": "^0.0.84",
"@definitelytyped/utils": "^0.0.84",
"@types/node": "^14.14.35",
"charm": "^1.0.2",
"fs-extra": "^8.1.0",
"fstream": "^1.0.12",
"npm-registry-client": "^8.6.0",
"strip-json-comments": "^4.0.0",
"tar": "^2.2.2",
"tar-stream": "^2.1.4",
"typescript": "^4.1.0"
},
"devDependencies": {
"@types/charm": "^1.0.1",
"@types/fs-extra": "^8.1.0",
"@types/tar": "^4.0.3",
"@types/tar-stream": "^2.1.0"
},
"publishConfig": {
"access": "public"
}
}
52 changes: 52 additions & 0 deletions packages/linting/src/checkPackageJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { makeTypesVersionsForPackageJson } from "@definitelytyped/header-parser";
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import assert = require("assert");
import { pathExists } from "fs-extra";
import { join as joinPaths } from "path";

import { readJsonWithComments } from "./readJsonWithComments";

export async function checkPackageJson(
dirPath: string,
typesVersions: readonly TypeScriptVersion[],
): Promise<void> {
const pkgJsonPath = joinPaths(dirPath, "package.json");
const needsTypesVersions = typesVersions.length !== 0;
if (!await pathExists(pkgJsonPath)) {
if (needsTypesVersions) {
throw new Error(`${dirPath}: Must have 'package.json' for "typesVersions"`);
}
return;
}

const pkgJson = await readJsonWithComments(pkgJsonPath) as Record<string, unknown>;

if ((pkgJson as any).private !== true) {
throw new Error(`${pkgJsonPath} should set \`"private": true\``);
}

if (needsTypesVersions) {
assert.strictEqual((pkgJson as any).types, "index", `"types" in '${pkgJsonPath}' should be "index".`);
const expected = makeTypesVersionsForPackageJson(typesVersions);
assert.deepEqual((pkgJson as any).typesVersions, expected,
`"typesVersions" in '${pkgJsonPath}' is not set right. Should be: ${JSON.stringify(expected, undefined, 4)}`);
}

for (const key in pkgJson) { // tslint:disable-line forin
switch (key) {
case "private":
case "dependencies":
case "license":
// "private"/"typesVersions"/"types" checked above, "dependencies" / "license" checked by types-publisher,
break;
case "typesVersions":
case "types":
if (!needsTypesVersions) {
throw new Error(`${pkgJsonPath} doesn't need to set "${key}" when no 'ts3.x' directories exist.`);
}
break;
default:
throw new Error(`${pkgJsonPath} should not include field ${key}`);
}
}
}
98 changes: 98 additions & 0 deletions packages/linting/src/checkTsconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getCompilerOptions } from "./util";

export interface DefinitelyTypedInfo {
/** "../" or "../../" or "../../../". This should use '/' even on windows. */
readonly relativeBaseUrl: string;
}

export async function checkTsconfig(dirPath: string, dt: DefinitelyTypedInfo | undefined): Promise<void> {
const options = await getCompilerOptions(dirPath);

if (dt) {
const { relativeBaseUrl } = dt;

const mustHave = {
module: "commonjs",
noEmit: true,
forceConsistentCasingInFileNames: true,
baseUrl: relativeBaseUrl,
typeRoots: [relativeBaseUrl],
types: [],
};

for (const key of Object.getOwnPropertyNames(mustHave) as (keyof typeof mustHave)[]) {
const expected = mustHave[key];
const actual = options[key];
if (!deepEquals(expected, actual)) {
throw new Error(`Expected compilerOptions[${JSON.stringify(key)}] === ${JSON.stringify(expected)}`);
}
}

for (const key in options) { // tslint:disable-line forin
switch (key) {
case "lib":
case "noImplicitAny":
case "noImplicitThis":
case "strict":
case "strictNullChecks":
case "noUncheckedIndexedAccess":
case "strictFunctionTypes":
case "esModuleInterop":
case "allowSyntheticDefaultImports":
// Allow any value
break;
case "target":
case "paths":
case "jsx":
case "jsxFactory":
case "experimentalDecorators":
case "noUnusedLocals":
case "noUnusedParameters":
// OK. "paths" checked further by types-publisher
break;
default:
if (!(key in mustHave)) {
throw new Error(`Unexpected compiler option ${key}`);
}
}
}
}

if (!("lib" in options)) {
throw new Error('Must specify "lib", usually to `"lib": ["es6"]` or `"lib": ["es6", "dom"]`.');
}

if ("strict" in options) {
if (options.strict !== true) {
throw new Error('When "strict" is present, it must be set to `true`.');
}

for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
if (key in options) {
throw new TypeError(`Expected "${key}" to not be set when "strict" is \`true\`.`);
}
}
} else {
for (const key of ["noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes"]) {
if (!(key in options)) {
throw new Error(`Expected \`"${key}": true\` or \`"${key}": false\`.`);
}
}
}

if (options.types && options.types.length) {
throw new Error(
'Use `/// <reference types="..." />` directives in source files and ensure ' +
'that the "types" field in your tsconfig is an empty array.');
}
}

function deepEquals(expected: unknown, actual: unknown): boolean {
if (expected instanceof Array) {
return actual instanceof Array
&& actual.length === expected.length
&& expected.every((e, i) => deepEquals(e, actual[i]));
} else {
return expected === actual;
}
}
29 changes: 29 additions & 0 deletions packages/linting/src/checkTslintJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { pathExists } from "fs-extra";
import { join as joinPaths } from "path";

import { readJsonWithComments } from "./readJsonWithComments";

export async function checkTslintJson(dirPath: string, dt: boolean): Promise<void> {
const configPath = getConfigPath(dirPath);
const shouldExtend = `dtslint/${dt ? "dt" : "dtslint"}.json`;
const validateExtends = (extend: string | string[]) =>
extend === shouldExtend || (!dt && Array.isArray(extend) && extend.some(val => val === shouldExtend));

if (!await pathExists(configPath)) {
if (dt) {
throw new Error(
`On DefinitelyTyped, must include \`tslint.json\` containing \`{ "extends": "${shouldExtend}" }\`.\n` +
"This was inferred as a DefinitelyTyped package because it contains a `// Type definitions for` header.");
}
return;
}

const tslintJson = await readJsonWithComments(configPath);
if (!validateExtends(tslintJson.extends)) {
throw new Error(`If 'tslint.json' is present, it should extend "${shouldExtend}"`);
}
}

function getConfigPath(dirPath: string): string {
return joinPaths(dirPath, "tslint.json");
}
35 changes: 35 additions & 0 deletions packages/linting/src/getProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { existsSync, readFileSync } from "fs";
import { dirname, resolve as resolvePath } from "path";
import * as TsType from "typescript";

const programCache = new WeakMap<TsType.Program, Map<string, TsType.Program>>();

/** Maps a Program to one created with the version specified in `options`. */
export function getProgram(configFile: string, ts: typeof TsType, versionName: string, lintProgram: TsType.Program): TsType.Program {
let versionToProgram = programCache.get(lintProgram);
if (versionToProgram === undefined) {
versionToProgram = new Map<string, TsType.Program>();
programCache.set(lintProgram, versionToProgram);
}

let newProgram = versionToProgram.get(versionName);
if (newProgram === undefined) {
newProgram = createProgram(configFile, ts);
versionToProgram.set(versionName, newProgram);
}
return newProgram;
}

function createProgram(configFile: string, ts: typeof TsType): TsType.Program {
const projectDirectory = dirname(configFile);
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);
const parseConfigHost: TsType.ParseConfigHost = {
fileExists: existsSync,
readDirectory: ts.sys.readDirectory,
readFile: file => readFileSync(file, "utf8"),
useCaseSensitiveFileNames: true,
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, resolvePath(projectDirectory), { noEmit: true });
const host = ts.createCompilerHost(parsed.options, true);
return ts.createProgram(parsed.fileNames, parsed.options, host);
}
4 changes: 4 additions & 0 deletions packages/linting/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./checkPackageJson";
export * from "./checkTsconfig";
export * from "./checkTslintJson";
export * from "./testDependencies";
7 changes: 7 additions & 0 deletions packages/linting/src/readJsonWithComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFile } from "fs-extra";
import stripJsonComments from "strip-json-comments";

export async function readJsonWithComments(path: string) {
const text = await readFile(path, "utf-8");
return JSON.parse(stripJsonComments(text));
}
68 changes: 68 additions & 0 deletions packages/linting/src/testDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { TypeScriptVersion } from "@definitelytyped/typescript-versions";
import { typeScriptPath } from "@definitelytyped/utils";
import assert = require("assert");
import { dirname, join as joinPaths, normalize } from "path";
import * as TsType from "typescript";

import { getProgram } from "./getProgram";

export type TsVersion = TypeScriptVersion | "local";

export function testDependencies(
version: TsVersion,
dirPath: string,
lintProgram: TsType.Program,
tsLocal: string | undefined,
): string | undefined {
const tsconfigPath = joinPaths(dirPath, "tsconfig.json");
assert(version !== "local" || tsLocal);
const ts: typeof TsType = require(typeScriptPath(version, tsLocal));
const program = getProgram(tsconfigPath, ts, version, lintProgram);
const diagnostics = ts.getPreEmitDiagnostics(program).filter(d => !d.file || isExternalDependency(d.file, dirPath, program));
if (!diagnostics.length) { return undefined; }

const showDiags = ts.formatDiagnostics(diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory: () => dirPath,
getNewLine: () => "\n",
});

const message = `Errors in typescript@${version} for external dependencies:\n${showDiags}`;

// Add an edge-case for someone needing to `npm install` in react when they first edit a DT module which depends on it - #226
const cannotFindDepsDiags = diagnostics.find(d => d.code === 2307 && d.messageText.toString().includes("Cannot find module"));
if (cannotFindDepsDiags && cannotFindDepsDiags.file) {
const path = cannotFindDepsDiags.file.fileName;
const typesFolder = dirname(path);

return `
A module look-up failed, this often occurs when you need to run \`npm install\` on a dependent module before you can lint.

Before you debug, first try running:

npm install --prefix ${typesFolder}

Then re-run. Full error logs are below.

${message}`;
} else {
return message;
}
}

function isExternalDependency(file: TsType.SourceFile, dirPath: string, program: TsType.Program): boolean {
return !startsWithDirectory(file.fileName, dirPath) || program.isSourceFileFromExternalLibrary(file);
}

function normalizePath(file: string) {
// replaces '\' with '/' and forces all DOS drive letters to be upper-case
return normalize(file)
.replace(/\\/g, "/")
.replace(/^[a-z](?=:)/, c => c.toUpperCase());
}

function startsWithDirectory(filePath: string, dirPath: string): boolean {
const normalFilePath = normalizePath(filePath);
const normalDirPath = normalizePath(dirPath).replace(/\/$/, "");
return normalFilePath.startsWith(normalDirPath + "/") || normalFilePath.startsWith(normalDirPath + "\\");
}
13 changes: 13 additions & 0 deletions packages/linting/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { pathExists } from "fs-extra";
import { join } from "path";
import * as ts from "typescript";

import { readJsonWithComments } from "./readJsonWithComments";

export async function getCompilerOptions(dirPath: string): Promise<ts.CompilerOptions> {
const tsconfigPath = join(dirPath, "tsconfig.json");
if (!await pathExists(tsconfigPath)) {
throw new Error(`Need a 'tsconfig.json' file in ${dirPath}`);
}
return (await readJsonWithComments(tsconfigPath)).compilerOptions;
}
9 changes: 9 additions & 0 deletions packages/linting/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../header-parser" }, { "path": "../typescript-versions" }, { "path": "../utils" }]
}
Loading