Files
portifolio-evandrodevbr/lib/jogo/complexidade.test.ts
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
2026-11-10 19:37:10 -03:00

161 lines
4.9 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import ts from "typescript";
const LIMITE = 10;
const PASTA = __dirname;
type Medida = { nome: string; linha: number; complexidade: number };
const OPERADORES = new Set([
ts.SyntaxKind.AmpersandAmpersandToken,
ts.SyntaxKind.BarBarToken,
ts.SyntaxKind.QuestionQuestionToken,
]);
function ehDecisao(no: ts.Node): boolean {
if (ts.isBinaryExpression(no)) return OPERADORES.has(no.operatorToken.kind);
return (
ts.isIfStatement(no) ||
ts.isConditionalExpression(no) ||
ts.isForStatement(no) ||
ts.isForInStatement(no) ||
ts.isForOfStatement(no) ||
ts.isWhileStatement(no) ||
ts.isDoStatement(no) ||
ts.isCaseClause(no) ||
ts.isCatchClause(no)
);
}
function ehFuncao(no: ts.Node): no is ts.FunctionLikeDeclaration {
return (
ts.isFunctionDeclaration(no) ||
ts.isFunctionExpression(no) ||
ts.isArrowFunction(no) ||
ts.isMethodDeclaration(no) ||
ts.isConstructorDeclaration(no) ||
ts.isGetAccessorDeclaration(no) ||
ts.isSetAccessorDeclaration(no)
);
}
function nomeDaFuncao(no: ts.FunctionLikeDeclaration): string {
if (ts.isConstructorDeclaration(no)) return "constructor";
if (no.name) return no.name.getText();
const pai = no.parent;
if (ts.isVariableDeclaration(pai) || ts.isPropertyAssignment(pai)) {
return pai.name.getText();
}
return "(anônima)";
}
/** Complexidade ciclomática de cada função do código-fonte (cada uma medida à parte). */
export function medirComplexidade(codigo: string): Medida[] {
const fonte = ts.createSourceFile(
"medida.ts",
codigo,
ts.ScriptTarget.Latest,
true,
);
const medidas: Medida[] = [];
function medir(funcao: ts.FunctionLikeDeclaration): void {
let complexidade = 1;
const visitar = (no: ts.Node): void => {
if (ehFuncao(no)) {
medir(no);
return;
}
if (ehDecisao(no)) complexidade++;
ts.forEachChild(no, visitar);
};
ts.forEachChild(funcao, visitar);
const { line } = fonte.getLineAndCharacterOfPosition(funcao.getStart());
medidas.push({
nome: nomeDaFuncao(funcao),
linha: line + 1,
complexidade,
});
}
const percorrer = (no: ts.Node): void => {
if (ehFuncao(no)) medir(no);
else ts.forEachChild(no, percorrer);
};
percorrer(fonte);
return medidas.sort((a, b) => a.linha - b.linha);
}
function comDecisoes(n: number): string {
const ifs = Array.from({ length: n }, (_, i) => `if (x > ${i}) y++;`);
return `function f(x: number) { let y = 0; ${ifs.join(" ")} return y; }`;
}
describe("medirComplexidade", () => {
it("função sem decisão vale 1", () => {
expect(
medirComplexidade("function f() { return 1; }")[0].complexidade,
).toBe(1);
});
it("10 decisões valem 11 e são reprovadas", () => {
const [medida] = medirComplexidade(comDecisoes(10));
expect(medida.complexidade).toBe(11);
expect(medida.complexidade > LIMITE).toBe(true);
});
it("9 decisões valem 10 e passam", () => {
const [medida] = medirComplexidade(comDecisoes(9));
expect(medida.complexidade).toBe(10);
expect(medida.complexidade > LIMITE).toBe(false);
});
it("decisões de função aninhada não contam na de fora", () => {
const medidas = medirComplexidade(
"function fora(a: number) { const dentro = () => { if (a) return 1; return a ? 2 : 3; }; return dentro; }",
);
expect(medidas.find((m) => m.nome === "fora")?.complexidade).toBe(1);
expect(medidas.find((m) => m.nome === "dentro")?.complexidade).toBe(3);
});
it("default não conta, case conta", () => {
const [medida] = medirComplexidade(
"function f(x: number) { switch (x) { case 1: return 1; case 2: return 2; default: return 0; } }",
);
expect(medida.complexidade).toBe(3);
});
it("&&, || e ?? contam", () => {
const [medida] = medirComplexidade(
"function f(a: any, b: any, c: any) { return (a && b) || (c ?? a); }",
);
expect(medida.complexidade).toBe(4);
});
it("laços, catch, do e ternário contam; métodos são medidos", () => {
const medidas = medirComplexidade(
"class A { m(l: number[]) { for (const i of l) {} for (const k in l) {} for (;;) {} while (1) {} do {} while (0); try {} catch {} return l ? 1 : 2; } }",
);
expect(medidas[0]).toMatchObject({ nome: "m", complexidade: 8 });
});
});
describe("complexidade ciclomática de lib/jogo", () => {
it(`nenhuma função passa de ${LIMITE}`, () => {
const reprovadas: string[] = [];
const arquivos = readdirSync(PASTA).filter(
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts"),
);
for (const arquivo of arquivos) {
const codigo = readFileSync(join(PASTA, arquivo), "utf8");
for (const m of medirComplexidade(codigo)) {
if (m.complexidade > LIMITE) {
reprovadas.push(`${arquivo}:${m.linha} ${m.nome} ${m.complexidade}`);
}
}
}
expect(reprovadas).toEqual([]);
});
});