Skip to content

Commit

Permalink
feat: add spell-checker-filename (#551)
Browse files Browse the repository at this point in the history
Добавляем экшон для проверки названий файлов. Если есть проблемы выдает предупреждение в PR-е
  • Loading branch information
SevereCloud authored Jan 28, 2025
1 parent 8e5daf6 commit 76960e2
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 1 deletion.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"packages": [
"VKUI/*",
"vkui-tokens/*",
"shared/rust/cargo-update-toml"
"shared/rust/cargo-update-toml",
"shared/*"
]
},
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions shared/spell-checker-filename/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
root: false,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: {
'@typescript-eslint/naming-convention': 'off', // [Reason] 'snake_case' is expected naming
},
};
9 changes: 9 additions & 0 deletions shared/spell-checker-filename/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: 'Spell Checker filename'
description: 'Spell Checker filename'
inputs:
token:
required: false
description: 'token with access to your repository'
runs:
using: 'node20'
main: 'dist/index.js'
3 changes: 3 additions & 0 deletions shared/spell-checker-filename/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from '../../jest.config';

export default config;
23 changes: 23 additions & 0 deletions shared/spell-checker-filename/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@actions-internal/spell-checker-filename",
"version": "0.0.0",
"main": "src/main.ts",
"license": "MIT",
"private": true,
"devDependencies": {
"@types/node": "^22.10.7",
"@types/nspell": "^2.1.6",
"typescript": "^5.7.3"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0",
"async-mutex": "^0.5.0",
"nspell": "^2.1.5"
},
"scripts": {
"prebuild": "shx rm -rf dist/*",
"build": "esbuild ./src/main.ts --bundle --outfile=dist/index.js --platform=node --packages=bundle",
"test": "jest --passWithNoTests"
}
}
33 changes: 33 additions & 0 deletions shared/spell-checker-filename/src/entities/repositories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface SpellCheckerRepository {
/**
* Проверяет слово на ошибки
*/
correct(word: string): Promise<boolean>;

/**
* Предлагает варианты исправления слова
*/
suggest(word: string): Promise<string[]>;

/**
* Добавляет слова в словарь
*/
addToDict(words: string[]): Promise<void>;
}

export interface GithubRepository {
/**
* Возвращает список измененных файлов из pull request'а
*/
pullRequestPaths(): Promise<string[]>;

/**
* Создает предупреждение для файла в pull request'е
*/
warningFile(path: string, message: string): Promise<void>;
}

export interface Repositories {
spellCheckerRepository: SpellCheckerRepository;
githubRepository: GithubRepository;
}
28 changes: 28 additions & 0 deletions shared/spell-checker-filename/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as core from '@actions/core';
import { NSpellSpellChecker } from './repositories/spell';
import { GitHub } from './repositories/github';
import { ActionService } from './service/action';
import { Repositories } from './entities/repositories';

function prodRepositories(): Repositories {
return {
spellCheckerRepository: new NSpellSpellChecker(),
githubRepository: new GitHub(),
};
}

async function main(): Promise<void> {
try {
const repositories: Repositories = prodRepositories();

const action = new ActionService(repositories);

await action.run();
} catch (error) {
if (error instanceof Error) {
core.setFailed(error.message);
}
}
}

void main();
60 changes: 60 additions & 0 deletions shared/spell-checker-filename/src/repositories/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as core from '@actions/core';
import * as github from '@actions/github';

import { GithubRepository } from '../entities/repositories';

export class GitHub implements GithubRepository {
private readonly octokit: ReturnType<typeof github.getOctokit>;

public constructor() {
const token = core.getInput('token', { required: true });
this.octokit = github.getOctokit(token);
}

public async pullRequestPaths(): Promise<string[]> {
if (github.context.payload.pull_request === undefined) {
throw new Error('Not found information about Pull Request');
}

const response = await this.octokit.graphql<{
repository: {
pullRequest: {
files: {
nodes: Array<{
path: string;
}>;
};
};
};
}>(
`
query($owner:String!, $repo: String!, $pull_number: Int!, $first: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pull_number) {
files(first:$first) {
nodes {
path
}
}
}
}
}
`,
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: github.context.payload.pull_request.number,
first: 100,
},
);

return response.repository.pullRequest.files.nodes.map((file) => file.path);
}

public async warningFile(path: string, message: string): Promise<void> {
core.warning(message, {
title: 'Проверка опечаток',
file: path,
});
}
}
25 changes: 25 additions & 0 deletions shared/spell-checker-filename/src/repositories/spell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from '@jest/globals';

import { NSpellSpellChecker } from './spell';

const spellChecker = new NSpellSpellChecker();

test('NSpellSpellChecker check', async () => {
expect(await spellChecker.correct('hello')).toBeTruthy();
expect(await spellChecker.correct('helloo')).toBeFalsy();
});

test('NSpellSpellChecker check personal dictionary', async () => {
expect(await spellChecker.correct('svg')).toBeTruthy();
expect(await spellChecker.correct('src')).toBeTruthy();
});

test('NSpellSpellChecker suggest', async () => {
expect(await spellChecker.suggest('helloo')).toEqual(['hello', 'halloo', 'hellos']);
});

test('NSpellSpellChecker check', async () => {
expect(await spellChecker.correct('npm')).toBeFalsy();
await spellChecker.addToDict(['npm']);
expect(await spellChecker.correct('npm')).toBeTruthy();
});
74 changes: 74 additions & 0 deletions shared/spell-checker-filename/src/repositories/spell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import nspell from 'nspell';
import { Mutex } from 'async-mutex';
import { SpellCheckerRepository } from '../entities/repositories';

const PERSONAL_DICTIONARY = ['svg', 'src'].join('\n');

export class NSpellSpellChecker implements SpellCheckerRepository {
private spell: ReturnType<typeof nspell> | null = null;
private readonly mutexLoad = new Mutex();

private async loadNSpell() {
// MIT словарь для проверки на английском языке
const urlEnDict =
'https://raw.githubusercontent.com/wooorm/dictionaries/8cfea406b505e4d7df52d5a19bce525df98c54ab/dictionaries/en/';

// TODO: Кэширование на устройстве?
const aff = await fetch(urlEnDict + 'index.aff');
const dic = await fetch(urlEnDict + 'index.dic');

this.spell = nspell(Buffer.from(await aff.arrayBuffer()), Buffer.from(await dic.arrayBuffer()));
this.spell.personal(PERSONAL_DICTIONARY);
}

private async load() {
/**
* Блокируем мьютексом, чтобы словарь не загружался несколько раз одновременно
*/
const release = await this.mutexLoad.acquire();

if (!this.spell) {
await this.loadNSpell();
}

release();
}

public async correct(word: string): Promise<boolean> {
await this.load();

return this.spell!.correct(word);
}

public async suggest(word: string): Promise<string[]> {
await this.load();

return this.spell!.suggest(word);
}

public async addToDict(words: string[]): Promise<void> {
await this.load();

this.spell!.personal(words.join('\n'));
}
}

export class MockSpellChecker implements SpellCheckerRepository {
public dict = new Set<string>();
public suggestMap = new Map<string, string[]>();

public async correct(word: string): Promise<boolean> {
return this.dict.has(word);
}

public async suggest(word: string): Promise<string[]> {
return this.suggestMap.get(word) ?? [];
}

public async addToDict(words: string[]): Promise<void> {
words.forEach((word) => {
this.dict.add(word);
});
}
}
18 changes: 18 additions & 0 deletions shared/spell-checker-filename/src/service/action.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from '@jest/globals';
import { ActionService } from './action';
import { MockSpellChecker } from '../repositories/spell';

test('Action checkPath', async () => {
const repositories = {
spellCheckerRepository: new MockSpellChecker(),
githubRepository: {} as any,
};

repositories.spellCheckerRepository.dict = new Set(['path', 'to', 'file', 'svg']);

const action = new ActionService(repositories);

expect(await action.checkPath('path/to_file')).toEqual([]);
expect(await action.checkPath('path/to_file/40.svg')).toEqual([]);
expect(await action.checkPath('path/to_file/bad')).toEqual(['bad']);
});
41 changes: 41 additions & 0 deletions shared/spell-checker-filename/src/service/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Service } from './service';

export class ActionService extends Service {
/**
* Проверяет путь на наличие ошибок в словах
*/
public async checkPath(path: string): Promise<string[]> {
const result: string[] = [];
const words = path
.replace(/[-_\/\d\.]/g, ' ')
.split(' ')
.filter((word) => word);

for (const word of words) {
if (await this.repositories.spellCheckerRepository.correct(word)) {
continue;
}

result.push(word);
}

return result;
}

/**
* Запускает проверку всех путей из пулл реквеста
*/
public async run(): Promise<void> {
const paths = await this.repositories.githubRepository.pullRequestPaths();

for await (const filePath of paths) {
const word = await this.checkPath(filePath);
if (!word.length) continue;

await this.repositories.githubRepository.warningFile(
filePath,
`Возможно ошибочное написание слова '${word.join(' ')}' в пути к файлу ${filePath}`,
);
}
}
}
12 changes: 12 additions & 0 deletions shared/spell-checker-filename/src/service/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Repositories } from '../entities/repositories';

/**
* Базовый класс сервиса, для доступа к [адаптерам](../repositories/)
*/
export class Service {
protected readonly repositories: Repositories;

public constructor(repositories: Repositories) {
this.repositories = repositories;
}
}
15 changes: 15 additions & 0 deletions shared/spell-checker-filename/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2015",
"moduleResolution": "node",
"module": "commonjs",
"noEmit": true,
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts", ".eslintrc.js"],
"exclude": ["node_modules"]
}
Loading

0 comments on commit 76960e2

Please sign in to comment.