Exercício 1
Troque &Vec por slice sem mudar a contagem
stub.rs
#[allow(
clippy::ptr_arg,
reason = "o exercício começa com a API que será refatorada"
)]
pub fn count_visible(names: &Vec<String>) -> usize {
names.iter().filter(|name| !name.trim().is_empty()).count()
}
harness.rs
fn json_escape(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn print_result(case: usize, pass: bool, input: &str, want: &str, got: &str) {
println!(
"RESULT {{\"case\":{case},\"pass\":{pass},\"input\":\"{}\",\"want\":\"{}\",\"got\":\"{}\"}}",
json_escape(input),
json_escape(want),
json_escape(got)
);
}
fn main() {
let cases = [
(
vec!["Ana", "", " Rui "],
"[\"Ana\", \"\", \" Rui \" ]",
2usize,
),
(vec![], "[]", 0usize),
(vec![" ", "Mia"], "[\" \", \"Mia\"]", 1usize),
];
for (index, (raw, input, want)) in cases.into_iter().enumerate() {
let names: Vec<String> = raw.into_iter().map(String::from).collect();
let got = count_visible(&names);
print_result(
index + 1,
got == want,
input,
&want.to_string(),
&got.to_string(),
);
}
}
cases.json
[
{
"case": 1,
"input": "[\"Ana\", \"\", \" Rui \" ]",
"want": "2"
},
{
"case": 2,
"input": "[]",
"want": "0"
},
{
"case": 3,
"input": "[\" \", \"Mia\"]",
"want": "1"
}
]
source-check.mjs
import { readFileSync } from 'node:fs';
const requirements = {
"required": [
{
"label": "count_visible é uma função pública que recebe slice",
"pattern": "pub\\s+fn\\s+count_visible\\s*\\(\\s*names\\s*:\\s*&\\s*\\[\\s*String\\s*\\]"
}
],
"forbidden": [
{
"label": "a exceção antiga de ptr_arg foi removida",
"pattern": "clippy\\s*::\\s*ptr_arg"
}
],
"scopes": [
{
"label": "função pública count_visible",
"start_pattern": "pub\\s+fn\\s+count_visible\\s*\\(\\s*names\\s*:\\s*&\\s*\\[\\s*String\\s*\\]",
"required": [
{
"label": "count_visible filtra nomes visíveis",
"pattern": "names\\s*\\.\\s*iter\\s*\\(\\s*\\)[\\s\\S]*?\\.\\s*filter\\s*\\("
}
],
"forbidden": [
{
"label": "count_visible não recria um Vec",
"pattern": "\\bVec\\s*::|\\.\\s*(?:to_vec|to_owned|collect)\\s*\\("
}
]
}
]
};
const file = process.argv[2] || 'stub.rs';
const source = readFileSync(file, 'utf8');
function rustCodeOnly(source) {
let output = '';
let index = 0;
let blockDepth = 0;
let state = 'code';
let rawEnd = '';
while (index < source.length) {
const current = source[index];
const next = source[index + 1];
if (state === 'line-comment') {
if (current === '\n') { state = 'code'; output += '\n'; } else output += ' ';
index += 1;
continue;
}
if (state === 'block-comment') {
if (current === '/' && next === '*') { blockDepth += 1; output += ' '; index += 2; }
else if (current === '*' && next === '/') {
blockDepth -= 1; output += ' '; index += 2;
if (blockDepth === 0) state = 'code';
} else { output += current === '\n' ? '\n' : ' '; index += 1; }
continue;
}
if (state === 'string') {
if (current === '\\') { output += ' '; index += Math.min(2, source.length - index); }
else { output += current === '\n' ? '\n' : ' '; index += 1; if (current === '"') state = 'code'; }
continue;
}
if (state === 'raw-string') {
if (source.startsWith(rawEnd, index)) {
output += ' '.repeat(rawEnd.length); index += rawEnd.length; state = 'code';
} else { output += current === '\n' ? '\n' : ' '; index += 1; }
continue;
}
if (current === '/' && next === '/') { state = 'line-comment'; output += ' '; index += 2; continue; }
if (current === '/' && next === '*') { state = 'block-comment'; blockDepth = 1; output += ' '; index += 2; continue; }
const raw = /^(?:br|r)(#+)?"/.exec(source.slice(index));
if (raw) {
const hashes = raw[1] || ''; rawEnd = '"' + hashes; state = 'raw-string';
output += ' '.repeat(raw[0].length); index += raw[0].length; continue;
}
if (current === 'b' && next === '"') { state = 'string'; output += ' '; index += 2; continue; }
const character = /^(?:b)?'(?:\\.|[^'\\])'/.exec(source.slice(index));
if (character) { output += ' '.repeat(character[0].length); index += character[0].length; continue; }
if (current === '"') { state = 'string'; output += ' '; index += 1; continue; }
output += current;
index += 1;
}
return output;
}
const code = rustCodeOnly(source);
const errors = [];
function evaluate(sourceCode, contract, prefix = '') {
for (const item of contract.required || []) {
const matches = [...sourceCode.matchAll(new RegExp(item.pattern, 'gms'))].length;
const minimum = item.min_count || 1;
if (matches < minimum) errors.push(`${prefix}ausente: ${item.label} (esperado ${minimum}, encontrado ${matches})`);
}
for (const item of contract.forbidden || []) {
const matches = [...sourceCode.matchAll(new RegExp(item.pattern, 'gms'))].length;
const minimum = item.min_count || 1;
if (matches >= minimum) errors.push(`${prefix}proibido: ${item.label} (limite ${minimum - 1}, encontrado ${matches})`);
}
for (const sequence of contract.ordered || []) {
let cursor = 0;
let valid = true;
for (const pattern of sequence.patterns) {
const match = new RegExp(pattern, 'ms').exec(sourceCode.slice(cursor));
if (!match) { valid = false; break; }
cursor += match.index + match[0].length;
}
if (!valid) errors.push(`${prefix}ordem inválida: ${sequence.label}`);
}
}
function itemBody(scope) {
const matches = [...code.matchAll(new RegExp(scope.start_pattern, 'gms'))];
if (matches.length !== 1) {
errors.push(`escopo ${scope.label} precisa identificar um item; encontrado ${matches.length}`);
return null;
}
const start = matches[0].index + matches[0][0].length;
let opening = -1;
for (let index = start; index < code.length; index += 1) {
if (code[index] === ';') { errors.push(`escopo ${scope.label} não possui corpo`); return null; }
if (code[index] === '{') { opening = index; break; }
}
if (opening === -1) { errors.push(`escopo ${scope.label} não possui chave de abertura`); return null; }
let depth = 0;
for (let index = opening; index < code.length; index += 1) {
if (code[index] === '{') depth += 1;
else if (code[index] === '}') {
depth -= 1;
if (depth === 0) return code.slice(opening + 1, index);
}
}
errors.push(`escopo ${scope.label} possui chaves sem fechamento`);
return null;
}
if (!requirements.allow_cfg && /#\s*!?\s*\[\s*cfg(?:_attr)?\b/ms.test(code)) {
errors.push('atributos cfg são proibidos neste exercício');
}
evaluate(code, requirements);
for (const scope of requirements.scopes || []) {
const body = itemBody(scope);
if (body !== null) evaluate(body, scope, `escopo ${scope.label}: `);
}
if (errors.length) {
console.error('Contrato de fonte não atendido:\n- ' + errors.join('\n- '));
process.exit(1);
}
console.log('Contrato de fonte atendido.');
Casos e valores esperados
| Caso | Entrada | Valor esperado |
|---|---|---|
| 1 | ["Ana", "", " Rui " ] |
2 |
| 2 | [] |
0 |
| 3 | [" ", "Mia"] |
1 |
Execute com o Cargo local
- Execute os comandos de preparação.
- Copie os arquivos acima para os arquivos vazios com os mesmos nomes.
- Execute a verificação de base. O stub deve mostrar o comportamento atual.
- Edite stub.rs. Depois, execute a verificação final da forma da fonte.
Comandos de preparação
cargo new --bin --edition 2024 jeitorust-01-contratos-nao-gosto-exercicio-1
cd jeitorust-01-contratos-nao-gosto-exercicio-1
touch stub.rs harness.rs cases.json source-check.mjs
printf 'include!("../stub.rs");\ninclude!("../harness.rs");\n' > src/main.rs
Verificação de base — confirme o comportamento atual antes da mudança
cargo fmt --check
cargo run --bin jeitorust-01-contratos-nao-gosto-exercicio-1
cargo clippy -- -D warnings
Verificação final — confirme a forma da fonte após a mudança
node source-check.mjs stub.rs
Mostrar dicas
- O corpo já preserva o comportamento; altere somente o tipo aceito na fronteira.
- Use names: &[String]; uma referência a Vec<String> sofre coerção para slice no chamador.
Revelar solução
solution.rs
pub fn count_visible(names: &[String]) -> usize {
names.iter().filter(|name| !name.trim().is_empty()).count()
}
O slice aceita Vec e outras sequências contíguas sem ampliar poderes da função; os casos confirmam a contagem, e o scanner confirma a sintaxe selecionada da fronteira.