Skip to content

Commit

Permalink
Added new linting package
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Aug 30, 2021
1 parent 1aa87f4 commit ab6e910
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 0 deletions.
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

0 comments on commit ab6e910

Please sign in to comment.