aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/iku/src/checker.test.ts37
-rw-r--r--packages/iku/src/checker.ts132
-rw-r--r--packages/iku/src/cli.ts5
-rw-r--r--packages/iku/src/index.ts7
4 files changed, 141 insertions, 40 deletions
diff --git a/packages/iku/src/checker.test.ts b/packages/iku/src/checker.test.ts
index 457d956..3116eb6 100644
--- a/packages/iku/src/checker.test.ts
+++ b/packages/iku/src/checker.test.ts
@@ -16,8 +16,7 @@ function createTempFile(content: string): string {
function cleanupTempFile(filePath: string): void {
try {
unlinkSync(filePath);
- } catch {
- }
+ } catch {}
}
describe("checkFile", () => {
@@ -68,7 +67,9 @@ const b = 2;
cleanupTempFile(filePath);
expect(errors).toHaveLength(1);
- expect(errors[0]).toContain("Unnecessary blank line between same statement types");
+ expect(errors[0]).toContain(
+ "Unnecessary blank line between same statement types",
+ );
});
it("passes for consecutive expression statements without blank lines", () => {
const content = `console.log("a");
@@ -105,7 +106,9 @@ console.log(a);
cleanupTempFile(filePath);
expect(errors).toHaveLength(1);
- expect(errors[0]).toContain("Missing blank line between different statement types");
+ expect(errors[0]).toContain(
+ "Missing blank line between different statement types",
+ );
});
it("requires blank line between variable declaration and return", () => {
const content = `function test() {
@@ -133,7 +136,7 @@ console.log(a);
cleanupTempFile(filePath);
expect(errors).toHaveLength(1);
- expect(errors[0]).toContain("expression -> if");
+ expect(errors[0]).toContain("Missing blank line around scoped block");
});
});
describe("scoped blocks", () => {
@@ -171,7 +174,7 @@ console.log(a);
cleanupTempFile(filePath);
expect(errors).toHaveLength(1);
- expect(errors[0]).toContain("Missing blank line between scoped blocks of same type");
+ expect(errors[0]).toContain("Missing blank line around scoped block");
});
it("passes when scoped for loops have blank lines between them", () => {
const content = `function test(items: number[]) {
@@ -205,7 +208,7 @@ console.log(a);
cleanupTempFile(filePath);
expect(errors).toHaveLength(1);
- expect(errors[0]).toContain("Missing blank line between scoped blocks of same type");
+ expect(errors[0]).toContain("Missing blank line around scoped block");
});
});
describe("nested blocks", () => {
@@ -290,7 +293,9 @@ const b = 2;
const errors = checkFile(filePath);
cleanupTempFile(filePath);
- expect(errors.some((error) => error.includes("Comment detected"))).toBe(true);
+ expect(errors.some((error) => error.includes("Comment detected"))).toBe(
+ true,
+ );
});
it("detects multi-line comments by default", () => {
const content = `const a = 1;
@@ -302,7 +307,9 @@ const b = 2;
const errors = checkFile(filePath);
cleanupTempFile(filePath);
- expect(errors.some((error) => error.includes("Comment detected"))).toBe(true);
+ expect(errors.some((error) => error.includes("Comment detected"))).toBe(
+ true,
+ );
});
it("detects trailing comments by default", () => {
const content = `const a = 1; // trailing comment
@@ -311,7 +318,9 @@ const b = 2;
const errors = checkFile(filePath);
cleanupTempFile(filePath);
- expect(errors.some((error) => error.includes("Comment detected"))).toBe(true);
+ expect(errors.some((error) => error.includes("Comment detected"))).toBe(
+ true,
+ );
});
it("allows comments when noComments is false", () => {
const content = `const a = 1;
@@ -322,7 +331,9 @@ const b = 2;
const errors = checkFile(filePath, { noComments: false });
cleanupTempFile(filePath);
- expect(errors.some((error) => error.includes("Comment detected"))).toBe(false);
+ expect(errors.some((error) => error.includes("Comment detected"))).toBe(
+ false,
+ );
});
it("detects JSDoc comments by default", () => {
const content = `/**
@@ -334,7 +345,9 @@ function test() {}
const errors = checkFile(filePath);
cleanupTempFile(filePath);
- expect(errors.some((error) => error.includes("Comment detected"))).toBe(true);
+ expect(errors.some((error) => error.includes("Comment detected"))).toBe(
+ true,
+ );
});
});
});
diff --git a/packages/iku/src/checker.ts b/packages/iku/src/checker.ts
index a69a232..f3e4a65 100644
--- a/packages/iku/src/checker.ts
+++ b/packages/iku/src/checker.ts
@@ -13,6 +13,7 @@ const defaultOptions: CheckerOptions = {
type StatementCategory =
| "import"
| "variableDeclaration"
+ | "typeAlias"
| "if"
| "for"
| "while"
@@ -37,11 +38,19 @@ function getStatementCategory(node: typescript.Statement): StatementCategory {
return "variableDeclaration";
}
+ if (typescript.isTypeAliasDeclaration(node)) {
+ return "typeAlias";
+ }
+
if (typescript.isIfStatement(node)) {
return "if";
}
- if (typescript.isForStatement(node) || typescript.isForInStatement(node) || typescript.isForOfStatement(node)) {
+ if (
+ typescript.isForStatement(node) ||
+ typescript.isForInStatement(node) ||
+ typescript.isForOfStatement(node)
+ ) {
return "for";
}
@@ -69,7 +78,11 @@ function statementHasBlock(node: typescript.Statement): boolean {
return typescript.isBlock(node.thenStatement);
}
- if (typescript.isForStatement(node) || typescript.isForInStatement(node) || typescript.isForOfStatement(node)) {
+ if (
+ typescript.isForStatement(node) ||
+ typescript.isForInStatement(node) ||
+ typescript.isForOfStatement(node)
+ ) {
return typescript.isBlock(node.statement);
}
@@ -81,6 +94,10 @@ function statementHasBlock(node: typescript.Statement): boolean {
return typescript.isBlock(node.statement);
}
+ if (typescript.isTypeAliasDeclaration(node)) {
+ return typescript.isTypeLiteralNode(node.type);
+ }
+
return false;
}
@@ -94,8 +111,10 @@ function getStatementsFromBlock(
const category = getStatementCategory(statement);
const startPosition = statement.getStart(sourceFile);
const endPosition = statement.getEnd();
- const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line + 1;
- const endLine = sourceFile.getLineAndCharacterOfPosition(endPosition).line + 1;
+ const startLine =
+ sourceFile.getLineAndCharacterOfPosition(startPosition).line + 1;
+ const endLine =
+ sourceFile.getLineAndCharacterOfPosition(endPosition).line + 1;
const hasBlock = statementHasBlock(statement);
statements.push({ category, startLine, endLine, hasBlock });
@@ -113,7 +132,11 @@ function countBlankLinesBetween(
const lines = text.split("\n");
let blankCount = 0;
- for (let lineIndex = previousEndLine; lineIndex < currentStartLine - 1; lineIndex++) {
+ for (
+ let lineIndex = previousEndLine;
+ lineIndex < currentStartLine - 1;
+ lineIndex++
+ ) {
const line = lines[lineIndex];
if (line !== undefined && line.trim() === "") {
@@ -132,13 +155,22 @@ function hasCommentBetween(
const text = sourceFile.getFullText();
const lines = text.split("\n");
- for (let lineIndex = previousEndLine; lineIndex < currentStartLine - 1; lineIndex++) {
+ for (
+ let lineIndex = previousEndLine;
+ lineIndex < currentStartLine - 1;
+ lineIndex++
+ ) {
const line = lines[lineIndex];
if (line !== undefined) {
const trimmed = line.trim();
- if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.endsWith("*/")) {
+ if (
+ trimmed.startsWith("//") ||
+ trimmed.startsWith("/*") ||
+ trimmed.startsWith("*") ||
+ trimmed.endsWith("*/")
+ ) {
return true;
}
}
@@ -167,33 +199,50 @@ function checkStatementSpacing(
continue;
}
- const blankLines = countBlankLinesBetween(previous.endLine, current.startLine, sourceFile);
+ const blankLines = countBlankLinesBetween(
+ previous.endLine,
+ current.startLine,
+ sourceFile,
+ );
const sameCategory = previous.category === current.category;
- const hasComment = hasCommentBetween(previous.endLine, current.startLine, sourceFile);
+ const hasComment = hasCommentBetween(
+ previous.endLine,
+ current.startLine,
+ sourceFile,
+ );
if (previous.category === "import" && current.category === "import") {
if (blankLines > 0 && !(allowComments && hasComment)) {
- errors.push(`${filePath}:${current.startLine}: Blank line between imports`);
+ errors.push(
+ `${filePath}:${current.startLine}: Blank line between imports`,
+ );
}
continue;
}
- const bothHaveBlocks = previous.hasBlock && current.hasBlock;
+ const eitherHasBlock = previous.hasBlock || current.hasBlock;
- if (sameCategory && !bothHaveBlocks && blankLines > 0 && !(allowComments && hasComment)) {
+ if (eitherHasBlock && blankLines === 0) {
errors.push(
- `${filePath}:${current.startLine}: Unnecessary blank line between same statement types (${current.category})`,
+ `${filePath}:${current.startLine}: Missing blank line around scoped block`,
);
+
+ continue;
}
- if (sameCategory && bothHaveBlocks && blankLines === 0) {
+ if (
+ sameCategory &&
+ !eitherHasBlock &&
+ blankLines > 0 &&
+ !(allowComments && hasComment)
+ ) {
errors.push(
- `${filePath}:${current.startLine}: Missing blank line between scoped blocks of same type (${current.category})`,
+ `${filePath}:${current.startLine}: Unnecessary blank line between same statement types (${current.category})`,
);
}
- if (!sameCategory && blankLines === 0 && !hasComment) {
+ if (!sameCategory && !eitherHasBlock && blankLines === 0 && !hasComment) {
errors.push(
`${filePath}:${current.startLine}: Missing blank line between different statement types (${previous.category} -> ${current.category})`,
);
@@ -212,7 +261,12 @@ function visitBlocks(
): void {
if (typescript.isBlock(node)) {
const statements = getStatementsFromBlock(node, sourceFile);
- const blockErrors = checkStatementSpacing(statements, sourceFile, filePath, allowComments);
+ const blockErrors = checkStatementSpacing(
+ statements,
+ sourceFile,
+ filePath,
+ allowComments,
+ );
errors.push(...blockErrors);
}
@@ -222,17 +276,27 @@ function visitBlocks(
});
}
-function checkForComments(sourceFile: typescript.SourceFile, filePath: string): string[] {
+function checkForComments(
+ sourceFile: typescript.SourceFile,
+ filePath: string,
+): string[] {
const errors: string[] = [];
const text = sourceFile.getFullText();
function visit(node: typescript.Node): void {
- const leadingComments = typescript.getLeadingCommentRanges(text, node.getFullStart());
- const trailingComments = typescript.getTrailingCommentRanges(text, node.getEnd());
+ const leadingComments = typescript.getLeadingCommentRanges(
+ text,
+ node.getFullStart(),
+ );
+ const trailingComments = typescript.getTrailingCommentRanges(
+ text,
+ node.getEnd(),
+ );
if (leadingComments) {
for (const comment of leadingComments) {
- const line = sourceFile.getLineAndCharacterOfPosition(comment.pos).line + 1;
+ const line =
+ sourceFile.getLineAndCharacterOfPosition(comment.pos).line + 1;
errors.push(`${filePath}:${line}: Comment detected`);
}
@@ -240,7 +304,8 @@ function checkForComments(sourceFile: typescript.SourceFile, filePath: string):
if (trailingComments) {
for (const comment of trailingComments) {
- const line = sourceFile.getLineAndCharacterOfPosition(comment.pos).line + 1;
+ const line =
+ sourceFile.getLineAndCharacterOfPosition(comment.pos).line + 1;
errors.push(`${filePath}:${line}: Comment detected`);
}
@@ -256,7 +321,10 @@ function checkForComments(sourceFile: typescript.SourceFile, filePath: string):
return uniqueErrors;
}
-export function checkFile(filePath: string, options: CheckerOptions = defaultOptions): string[] {
+export function checkFile(
+ filePath: string,
+ options: CheckerOptions = defaultOptions,
+): string[] {
const program = typescript.createProgram([filePath], {
target: typescript.ScriptTarget.ESNext,
module: typescript.ModuleKind.ESNext,
@@ -274,7 +342,12 @@ export function checkFile(filePath: string, options: CheckerOptions = defaultOpt
const allowComments = !options.noComments;
const errors: string[] = [];
const topLevelStatements = getStatementsFromBlock(sourceFile, sourceFile);
- const topLevelErrors = checkStatementSpacing(topLevelStatements, sourceFile, filePath, allowComments);
+ const topLevelErrors = checkStatementSpacing(
+ topLevelStatements,
+ sourceFile,
+ filePath,
+ allowComments,
+ );
errors.push(...topLevelErrors);
visitBlocks(sourceFile, sourceFile, filePath, errors, allowComments);
@@ -296,10 +369,17 @@ export function walkDirectory(directory: string): string[] {
const stat = statSync(fullPath);
if (stat.isDirectory()) {
- if (!entry.includes("node_modules") && !entry.includes("dist") && !entry.includes(".next")) {
+ if (
+ !entry.includes("node_modules") &&
+ !entry.includes("dist") &&
+ !entry.includes(".next")
+ ) {
files.push(...walkDirectory(fullPath));
}
- } else if ([".ts", ".tsx"].includes(extname(entry)) && !entry.endsWith(".d.ts")) {
+ } else if (
+ [".ts", ".tsx"].includes(extname(entry)) &&
+ !entry.endsWith(".d.ts")
+ ) {
files.push(fullPath);
}
}
diff --git a/packages/iku/src/cli.ts b/packages/iku/src/cli.ts
index 8ea07a4..aea9350 100644
--- a/packages/iku/src/cli.ts
+++ b/packages/iku/src/cli.ts
@@ -21,7 +21,10 @@ function findMonorepoRoot(startDirectory: string): string | null {
return null;
}
-function parseArguments(arguments_: string[]): { directory?: string; options: CheckerOptions } {
+function parseArguments(arguments_: string[]): {
+ directory?: string;
+ options: CheckerOptions;
+} {
const options: CheckerOptions = {
noComments: true,
};
diff --git a/packages/iku/src/index.ts b/packages/iku/src/index.ts
index d3d29ae..d175fb5 100644
--- a/packages/iku/src/index.ts
+++ b/packages/iku/src/index.ts
@@ -1 +1,6 @@
-export { checkFile, walkDirectory, checkDirectory, type CheckerOptions } from "./checker.js";
+export {
+ checkFile,
+ walkDirectory,
+ checkDirectory,
+ type CheckerOptions,
+} from "./checker.js";