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.
161 lines
4.9 KiB
TypeScript
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([]);
|
|
});
|
|
});
|