Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
prettier-config-released: ${{ steps.release.outputs['packages/perttier-config--release_created'] }}
code-style-released: ${{ steps.release.outputs['packages/cli--release_created'] }}
ast-grep-config-released: ${{ steps.release.outputs['packages/ast-grep-config--release_created'] }}
editorconfig-released: ${{ steps.release.outputs['packages/editorconfig--release_created'] }}
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
Expand Down Expand Up @@ -110,6 +111,19 @@ jobs:
npm publish pleaseai-prettier-config-*.tgz --provenance --access public
fi

# No build step: the package ships its .editorconfig as-is.
- name: Publish @pleaseai/editorconfig
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || needs.release-please.outputs.editorconfig-released == 'true') }}
working-directory: packages/editorconfig
run: |
VERSION=$(node -p "require('./package.json').version")
if npm view "@pleaseai/editorconfig@$VERSION" version 2>/dev/null; then
echo "⚠ @pleaseai/editorconfig@$VERSION already published, skipping"
else
bun pm pack
npm publish pleaseai-editorconfig-*.tgz --provenance --access public
fi

# No build step: the package ships YAML rules and READMEs as-is.
- name: Publish @pleaseai/ast-grep-config
id: publish-ast-grep-config
Expand Down
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"packages/eslint-config": "0.0.4",
"packages/perttier-config": "0.0.1",
"packages/cli": "0.0.3",
"packages/ast-grep-config": "0.0.1"
"packages/ast-grep-config": "0.0.1",
"packages/editorconfig": "0.0.1"
}
1 change: 1 addition & 0 deletions packages/ast-grep-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"engines": {
"node": ">=22.0.0"
},
"preferUnplugged": true,
"scripts": {
"test": "bun test && ast-grep test"
},
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ Yarn Plug'n'Play 프로젝트(프로젝트나 상위 워크스페이스 루트
`node_modules/@pleaseai/ast-grep-config/rules`가 설치되어 있으면 상위 디렉터리가
Plug'n'Play여도 `sgconfig.yml`을 작성합니다.

**editorconfig** 도구도 마찬가지입니다. Plug'n'Play에서는 `init`이
`node_modules`에서 `.editorconfig`를 복사할 수 없으므로 파일을 건너뛰고, Yarn의
모듈 해석으로 복사하는 명령을 출력합니다.

```bash
yarn node -e "const fs = require('fs'); fs.copyFileSync(require.resolve('@pleaseai/editorconfig/.editorconfig'), '.editorconfig', fs.constants.COPYFILE_EXCL)"
```

`please-style check`는 Plug'n'Play에서도 동작합니다.
`@pleaseai/ast-grep-config`가 `preferUnplugged`를 설정해 두었으므로, Yarn이 이
패키지를 ast-grep이 읽을 수 있는 실제 디렉터리로 설치합니다.

## `please-style check`

`check`는
Expand Down Expand Up @@ -127,6 +139,14 @@ bunx @pleaseai/code-style check --strict # 발견 사항이 하나라도 있
- Dart: `lib/a/b.dart` ↔ `test/a/b_test.dart`.
- Kotlin/Java: `src/main/kotlin/…/Foo.kt` ↔ `src/test/kotlin/…/FooTest.kt`
(`java`도 같음).
- Kotlin Multiplatform: `src/<target>Test/kotlin/…/FooTest.kt` ↔
`src/<any>Main/kotlin/…/Foo.kt`(`commonTest` ↔ `commonMain`이며,
`iosTest`는 중간 소스 셋인 `appleMain`의 코드도 대상으로 삼을 수 있습니다).
`src/commonMain/kotlin/`이 있거나, `src/<name>Main/kotlin/` 셋은 있지만
`src/main|test|testFixtures/kotlin/`이 없는 모듈이 대상입니다. 같은 셋의
`java/` 소스도 대상으로 삼습니다. `androidInstrumentedTest`,
`androidAndroidTest`, `integrationTest`는 검사하지 않습니다. 헬퍼 검사는
멀티플랫폼 모듈을 건너뜁니다(`src/testFixtures/`가 없기 때문입니다).
- 테스트 루트 바로 아래의 환경 구간은 닫힌 목록 `unit`, `nuxt`, `browser`,
`e2e`에 있을 때만 인정합니다. 첫 구간이 목록에 있으면 환경 구간으로도,
미러 경로의 일부로도 해석해 봅니다. `e2e/` 아래 테스트는 경로를 유도하지
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ packages in zip archives without `node_modules`. To use the rules, set
`node_modules/@pleaseai/ast-grep-config/rules` still gets `sgconfig.yml`, even
under an enclosing Plug'n'Play directory.

The same goes for the **editorconfig** tool: under Plug'n'Play `init` cannot
copy `.editorconfig` from `node_modules`, so it skips the file and prints the
command that copies it with Yarn's resolver:

```bash
yarn node -e "const fs = require('fs'); fs.copyFileSync(require.resolve('@pleaseai/editorconfig/.editorconfig'), '.editorconfig', fs.constants.COPYFILE_EXCL)"
```

`please-style check` itself works under Plug'n'Play: `@pleaseai/ast-grep-config`
sets `preferUnplugged`, so Yarn installs it as a real directory that ast-grep
can read.

## `please-style check`

`check` is the layer-3 path checker of
Expand Down Expand Up @@ -131,6 +143,9 @@ What it checks:
- Dart: `lib/a/b.dart` ↔ `test/a/b_test.dart`.
- Kotlin/Java: `src/main/kotlin/…/Foo.kt` ↔ `src/test/kotlin/…/FooTest.kt`
(same for `java`).
- Kotlin Multiplatform: `src/<target>Test/kotlin/…/FooTest.kt` ↔
`src/<any>Main/kotlin/…/Foo.kt` (`commonTest` ↔ `commonMain`, and an
`iosTest` may target an intermediate `appleMain`).
- An environment segment directly under the test root is allowed if it is on
the closed list `unit`, `nuxt`, `browser`, `e2e`. A first segment on the
list is tried both as an environment and as part of the mirrored path.
Expand Down Expand Up @@ -247,6 +262,14 @@ snake_case (Dart), for example `parseURL` → `parse-url.ts`.
- Kotlin and Java modules are directories that contain `src/main|test|testFixtures/<lang>/`;
tests are `*Test.kt`/`*Test.java`. A test may target a source in either main
source set: `src/main/kotlin/…` (`.kt`, `.java`) or `src/main/java/…` (`.java`, `.kt`).
- A Kotlin module with `src/commonMain/kotlin/`, or with `src/<name>Main/kotlin/`
sets and no `src/main|test|testFixtures/kotlin/`, is Kotlin Multiplatform:
every `src/<name>Test/kotlin/` set is a test root and every
`src/<name>Main/kotlin/` set (or its `java/` sibling) a source root.
`androidInstrumentedTest`, `androidAndroidTest` and `integrationTest` do not
mirror sources and are not checked; a `*Test.kt` inside a `*Main` set is
reported as outside the test root. The helper check skips multiplatform
modules (they have no `src/testFixtures/`).
- Test files that belong to no package or module (no enclosing `package.json`,
`pubspec.yaml` or `src/<set>/<lang>/` directory) are not checked. This is
reported as a notice, aggregated per language with the count and up to three
Expand Down
50 changes: 28 additions & 22 deletions packages/cli/src/appliers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,41 @@
return result
}

/**
* Yarn Plug'n'Play installs packages as zip archives without `node_modules`, so a package file read from
* `node_modules` does not exist. A workspace package's `.pnp.cjs` lives at the workspace root, so the enclosing
* directories are searched too; an `installed` path that exists means this project is not under Plug'n'Play,
* whatever an ancestor holds.
*/
function usesPlugNPlay(cwd: string, installed: string): boolean {
if (existsSync(joinPath(cwd, installed))) {

Check warning on line 89 in packages/cli/src/appliers.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/appliers.ts#L89

Found existsSync from package "node:fs" with non literal argument at index 0
return false
}
for (let dir = joinPath(cwd); ; dir = dirname(dir)) {
if (existsSync(joinPath(dir, '.pnp.cjs')) || existsSync(joinPath(dir, '.pnp.js'))) {
return true
}
if (dirname(dir) === dir) {
return false
}
}
}

// --- editorconfig -----------------------------------------------------------

const EDITORCONFIG_SOURCE = 'node_modules/@pleaseai/editorconfig/.editorconfig'

async function applyEditorConfig(ctx: ToolContext): Promise<ToolApplyResult> {
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
const target = joinPath(ctx.cwd, '.editorconfig')
const source = joinPath(ctx.cwd, 'node_modules', '@pleaseai', 'editorconfig', '.editorconfig')
const source = joinPath(ctx.cwd, EDITORCONFIG_SOURCE)
const label = '.editorconfig'

if (!existsSync(source)) {
// Package not installed yet (e.g. install step failed). Skip silently.
// Not installed (e.g. the install step failed), or installed by Plug'n'Play without a node_modules copy.
if (!existsSync(target) && usesPlugNPlay(ctx.cwd, EDITORCONFIG_SOURCE)) {

Check warning on line 114 in packages/cli/src/appliers.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/appliers.ts#L114

Found existsSync from package "node:fs" with non literal argument at index 0
log.warn(t('editorconfigPlugNPlay'))
}
result.skipped.push(label)
return result
}
Expand Down Expand Up @@ -228,30 +253,11 @@
return entries.some(e => normalizeRuleDir(e) === AST_GREP_RULES)
}

/**
* Yarn Plug'n'Play installs packages as zip archives without `node_modules`, so the rules path above does not exist.
* A workspace package's `.pnp.cjs` lives at the workspace root, so the enclosing directories are searched too;
* an installed rules directory means this project is not under Plug'n'Play, whatever an ancestor holds.
*/
function usesPlugNPlay(cwd: string): boolean {
if (existsSync(joinPath(cwd, AST_GREP_RULES))) {
return false
}
for (let dir = joinPath(cwd); ; dir = dirname(dir)) {
if (existsSync(joinPath(dir, '.pnp.cjs')) || existsSync(joinPath(dir, '.pnp.js'))) {
return true
}
if (dirname(dir) === dir) {
return false
}
}
}

export async function applyAstGrep(ctx: ToolContext): Promise<ToolApplyResult> {
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
const target = joinPath(ctx.cwd, 'sgconfig.yml')
const label = 'sgconfig.yml'
if (usesPlugNPlay(ctx.cwd)) {
if (usesPlugNPlay(ctx.cwd, AST_GREP_RULES)) {
log.warn(t('astGrepPlugNPlay'))
result.skipped.push(label)
return result
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/check/check-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ export function inspectHelpers(lang: HelperLanguage): HelperResult {
if (inDesignatedLocation(candidate.file, lang) || inFixtureProject(candidate.file, lang.units)) {
continue
}
const unit = unitOf(candidate.file, lang.units)
// A unit without a designated helper location (a multiplatform module) has nowhere to move a helper to.
if (unit != null && lang.layout.helperDirs(unit).length === 0) {
continue
}
const importers = new Set([...lang.importers(candidate)].filter(f => !inFixtureProject(f, lang.units)))
importers.delete(candidate.file)
if (importers.size < 2) {
continue
}
const unit = unitOf(candidate.file, lang.units)
if (unit == null) {
unitless.push(candidate.file)
continue
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/check/check-test-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function inspectUnitTestFile(file: string, unit: Unit, layout: TestLayout, fileS
}
const testRoot = unit.testRoots.find(root => isUnder(rel, root) && rel !== root)
if (testRoot == null) {
return layout.reportOutsideRoot(rel) ? outsideRootFinding(file, unit, layout) : null
return layout.reportOutsideRoot(rel, unit) ? outsideRootFinding(file, unit, layout) : null
}
if (layout.helperDirs(unit).some(dir => isUnder(rel, dir))) {
return null
Expand Down
58 changes: 44 additions & 14 deletions packages/cli/src/check/layouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
envSegments: string[]
/** Fixture projects (a `package.json` under a test root) relative to `dir`; their test files are test data. */
fixtureDirs?: string[]
/** A Kotlin Multiplatform module (`src/<target>Main`, `src/<target>Test` source sets). */
multiplatform?: boolean
}

/**
Expand All @@ -43,8 +45,8 @@
allowIndex: boolean
/** Designated shared-helper directory, relative to the unit (per test root). */
helperDirs: (unit: Unit) => string[]
/** Report a test file found in `rel` (relative to its unit) outside the test roots? */
reportOutsideRoot: (rel: string) => boolean
/** Report a test file found in `rel` (relative to `unit`) outside the test roots? */
reportOutsideRoot: (rel: string, unit: Unit) => boolean
}

export function isUnder(path: string, dir: string): boolean {
Expand Down Expand Up @@ -177,39 +179,67 @@

// --- Kotlin / Java (Gradle / Maven) -----------------------------------------

/** Kotlin Multiplatform test sets that do not mirror sources, like Android's `src/androidTest` or an `integrationTest` set. */
const KMP_UNMIRRORED_TESTS = new Set(['androidInstrumented', 'androidAndroid', 'integration'])
Comment thread
amondnet marked this conversation as resolved.

function jvmLayout(language: 'kotlin' | 'java', ext: '.kt' | '.java'): TestLayout {
// Gradle/Maven modules mix both languages: a test may target a source in the sibling-language main set.
const sibling = language === 'kotlin' ? 'java' : 'kotlin'
const siblingExt = ext === '.kt' ? '.java' : '.kt'
const test = new RegExp(`.Test\\${ext}$`)
const sourceSet = new RegExp(`^(?:(.*)/)?src/(?:main|test|testFixtures)/${language}/`)
// Kotlin Multiplatform source sets: `src/commonMain/kotlin`, `src/jvmTest/kotlin`, …
const kmpSet = new RegExp(String.raw`^(?:(.*)/)?src/(\w+)(Main|Test)/${language}/`)

Check failure on line 192 in packages/cli/src/check/layouts.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/check/layouts.ts#L192

Found RegExp with non literal argument

Check warning on line 192 in packages/cli/src/check/layouts.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/check/layouts.ts#L192

Found non-literal argument to RegExp Constructor

Check failure on line 192 in packages/cli/src/check/layouts.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/check/layouts.ts#L192

The `RegExp` constructor was called with a non-literal value.
return {
language,
units(files, base, rootDir) {
const dirs = new Set<string>()
// Module dir → KMP source set names (`commonMain`, `jvmTest`, …).
const kmpSets = new Map<string, Set<string>>()
for (const f of files) {
const m = sourceSet.exec(f)
if (m != null) {
dirs.add(m[1] ?? '')
}
const k = kmpSet.exec(f)
if (k != null && !(k[3] === 'Test' && KMP_UNMIRRORED_TESTS.has(k[2]))) {
const dir = k[1] ?? ''

Check warning on line 206 in packages/cli/src/check/layouts.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/cli/src/check/layouts.ts#L206

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.
kmpSets.set(dir, (kmpSets.get(dir) ?? new Set()).add(`${k[2]}${k[3]}`))
}
}
return [...dirs].map(dir => ({
dir,
testRoots: [`src/test/${language}`],
sourceRoots: [`src/main/${language}`],
alternateSourceRoots: [`src/main/${sibling}`],
keepRootName: false,
envSegments: withConfig(rootDir, dir, base).envSegments,
}))
// A module with `src/commonMain/<lang>`, or with `src/<target>Main/<lang>` sets and no classic source set
// (an empty commonMain is not listed), is multiplatform: every `*Test` set is a test root, and a test
// may target any `*Main` set (an `iosTest` may test code in an intermediate `appleMain`).
const multiplatform = new Set<string>()
for (const [dir, sets] of kmpSets) {
if (sets.has('commonMain') || (!dirs.has(dir) && [...sets].some(s => s.endsWith('Main')))) {
multiplatform.add(dir)
dirs.add(dir)
}
Comment thread
amondnet marked this conversation as resolved.
}
return [...dirs].map((dir) => {
const kmp = multiplatform.has(dir) ? [...kmpSets.get(dir) ?? []].sort((a, b) => a.localeCompare(b)) : null
const mains = kmp?.filter(s => s.endsWith('Main')) ?? []
return {
dir,
testRoots: kmp == null ? [`src/test/${language}`] : kmp.filter(s => s.endsWith('Test')).map(s => `src/${s}/${language}`),
sourceRoots: kmp == null ? [`src/main/${language}`] : mains.map(s => `src/${s}/${language}`),
alternateSourceRoots: kmp == null ? [`src/main/${sibling}`] : mains.map(s => `src/${s}/${sibling}`),
keepRootName: false,
envSegments: withConfig(rootDir, dir, base).envSegments,
multiplatform: kmp != null,
}
})
},
isTestFile: path => test.test(path),
sourceStem: fileName => fileName.slice(0, -`Test${ext}`.length),
sourceExtensions: [ext, siblingExt],
allowIndex: false,
helperDirs: () => ['src/testFixtures'],
// Only a test class inside production sources is misplaced; other source
// sets (androidTest, integrationTest, …) are their own test roots.
reportOutsideRoot: rel => rel.startsWith('src/main/'),
// Multiplatform modules have no `src/testFixtures`, so they have no designated helper location yet.
helperDirs: unit => unit.multiplatform === true ? [] : ['src/testFixtures'],
// Only a test class inside production sources (`src/main`, or a multiplatform `src/*Main`) is misplaced;
// other source sets (androidTest, integrationTest, …) are their own test roots.
reportOutsideRoot: (rel, unit) => rel.startsWith('src/main/') || (unit.multiplatform === true && /^src\/\w+Main\//.test(rel)),
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Messages {
updated: (file: string) => string
agentsBlockUpdated: string
astGrepPlugNPlay: string
editorconfigPlugNPlay: string
doctorHeading: string
doctorOk: string
doctorMissing: string
Expand Down Expand Up @@ -47,6 +48,7 @@ const messages: Record<Locale, Messages> = {
updated: file => `Updated ${file}`,
agentsBlockUpdated: 'Refreshed AGENTS.md pleaseai-code-style block.',
astGrepPlugNPlay: 'Yarn Plug\'n\'Play has no node_modules, so ast-grep cannot read the rules from node_modules/@pleaseai/ast-grep-config. Set `nodeLinker: node-modules` in .yarnrc.yml, run yarn install, and run init again.',
editorconfigPlugNPlay: 'Yarn Plug\'n\'Play has no node_modules, so .editorconfig was not copied from node_modules/@pleaseai/editorconfig. Copy it with: yarn node -e "const fs = require(\'fs\'); fs.copyFileSync(require.resolve(\'@pleaseai/editorconfig/.editorconfig\'), \'.editorconfig\', fs.constants.COPYFILE_EXCL)"',
doctorHeading: 'Project status',
doctorOk: 'OK',
doctorMissing: 'missing',
Expand Down Expand Up @@ -102,6 +104,7 @@ Exit codes: 0 ok or warnings only, 1 findings with --strict, 2 usage or config e
updated: file => `${file} 업데이트됨`,
agentsBlockUpdated: 'AGENTS.md의 pleaseai-code-style 블록을 갱신했습니다.',
astGrepPlugNPlay: 'Yarn Plug\'n\'Play는 node_modules를 만들지 않아 ast-grep이 node_modules/@pleaseai/ast-grep-config의 규칙을 읽을 수 없습니다. .yarnrc.yml에 `nodeLinker: node-modules`를 설정하고 yarn install 후 init을 다시 실행하세요.',
editorconfigPlugNPlay: 'Yarn Plug\'n\'Play는 node_modules를 만들지 않아 node_modules/@pleaseai/editorconfig에서 .editorconfig를 복사하지 못했습니다. 다음 명령으로 복사하세요: yarn node -e "const fs = require(\'fs\'); fs.copyFileSync(require.resolve(\'@pleaseai/editorconfig/.editorconfig\'), \'.editorconfig\', fs.constants.COPYFILE_EXCL)"',
doctorHeading: '프로젝트 상태',
doctorOk: '정상',
doctorMissing: '없음',
Expand Down
Loading
Loading