mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-18 12:30:07 +00:00
Compare commits
1 Commits
main
...
austin/com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d261e04bc |
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
pnpm exec lint-staged
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
pnpm exec tsx scripts/check-comment-commits.ts
|
||||
174
scripts/check-comment-commits.test.ts
Normal file
174
scripts/check-comment-commits.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
analyzeStagedDiff,
|
||||
commentSyntaxForFile,
|
||||
freshScanState,
|
||||
scanLine
|
||||
} from './check-comment-commits'
|
||||
|
||||
function diff(file: string, addedLines: string[], removedLines: string[] = []) {
|
||||
const body = [
|
||||
...removedLines.map((l) => `-${l}`),
|
||||
...addedLines.map((l) => `+${l}`)
|
||||
].join('\n')
|
||||
return [
|
||||
`diff --git a/${file} b/${file}`,
|
||||
`--- a/${file}`,
|
||||
`+++ b/${file}`,
|
||||
`@@ -1,${Math.max(removedLines.length, 1)} +1,${Math.max(addedLines.length, 1)} @@`,
|
||||
body
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
describe('scanLine', () => {
|
||||
const ts = { line: true, block: true, html: false }
|
||||
|
||||
it('classifies a full-line comment as comment-only', () => {
|
||||
const r = scanLine('// a note', freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(true)
|
||||
expect(r.hasCode).toBe(false)
|
||||
})
|
||||
|
||||
it('classifies plain code as code-only', () => {
|
||||
const r = scanLine('const x = 5', freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(false)
|
||||
expect(r.hasCode).toBe(true)
|
||||
})
|
||||
|
||||
it('flags a trailing inline comment as both code and comment', () => {
|
||||
const r = scanLine('const x = 5 // why', freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(true)
|
||||
expect(r.hasCode).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat // inside a string literal as a comment', () => {
|
||||
const r = scanLine("const u = 'http://comfy.org'", freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(false)
|
||||
expect(r.hasCode).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat an escaped slash in a regex literal as a comment', () => {
|
||||
const r = scanLine(
|
||||
"file = target.replace(/^b\\//, '')",
|
||||
freshScanState(),
|
||||
ts
|
||||
)
|
||||
expect(r.hasComment).toBe(false)
|
||||
expect(r.hasCode).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat a regex matching a comment marker as a comment', () => {
|
||||
const r = scanLine('const re = /^\\/\\*/', freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(false)
|
||||
expect(r.hasCode).toBe(true)
|
||||
})
|
||||
|
||||
it('treats JSDoc as a comment (no exemption)', () => {
|
||||
const r = scanLine('/** docs */', freshScanState(), ts)
|
||||
expect(r.hasComment).toBe(true)
|
||||
expect(r.hasCode).toBe(false)
|
||||
})
|
||||
|
||||
it('tracks block comments across lines', () => {
|
||||
const open = scanLine('/*', freshScanState(), ts)
|
||||
const mid = scanLine(' * still a comment', open.state, ts)
|
||||
expect(mid.hasComment).toBe(true)
|
||||
expect(mid.hasCode).toBe(false)
|
||||
const close = scanLine(' */', mid.state, ts)
|
||||
expect(close.hasComment).toBe(true)
|
||||
expect(close.state.inBlock).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('commentSyntaxForFile', () => {
|
||||
it('enables html comments for vue', () => {
|
||||
expect(commentSyntaxForFile('Foo.vue')?.html).toBe(true)
|
||||
})
|
||||
|
||||
it('disables line comments for css', () => {
|
||||
expect(commentSyntaxForFile('a.css')).toEqual({
|
||||
line: false,
|
||||
block: true,
|
||||
html: false
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores non-code files', () => {
|
||||
expect(commentSyntaxForFile('data.json')).toBeNull()
|
||||
expect(commentSyntaxForFile('readme.md')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('analyzeStagedDiff', () => {
|
||||
it('blocks code changes mixed with new comments', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('src/a.ts', ['const x = 5', '// explain x'])
|
||||
)
|
||||
expect(result.violation).toBe(true)
|
||||
expect(result.commentAdds).toEqual([
|
||||
{ file: 'src/a.ts', line: 2, text: '// explain x' }
|
||||
])
|
||||
})
|
||||
|
||||
it('blocks a trailing comment added to a code line', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('src/a.ts', ['const x = 5 // explain'])
|
||||
)
|
||||
expect(result.violation).toBe(true)
|
||||
})
|
||||
|
||||
it('allows a comment-only commit', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('src/a.ts', ['// a standalone note', '// another line'])
|
||||
)
|
||||
expect(result.violation).toBe(false)
|
||||
expect(result.hasCommentAdd).toBe(true)
|
||||
expect(result.hasCodeChange).toBe(false)
|
||||
})
|
||||
|
||||
it('allows a code-only commit', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('src/a.ts', ['const x = 5', 'const y = 6'])
|
||||
)
|
||||
expect(result.violation).toBe(false)
|
||||
})
|
||||
|
||||
it('blocks comments added in one file when code changes in another', () => {
|
||||
const combined = [
|
||||
diff('src/a.ts', ['const x = 5']),
|
||||
diff('src/b.ts', ['// just a note'])
|
||||
].join('\n')
|
||||
expect(analyzeStagedDiff(combined).violation).toBe(true)
|
||||
})
|
||||
|
||||
it('treats removing code as a code change', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('src/a.ts', ['// note'], ['const gone = 1'])
|
||||
)
|
||||
expect(result.violation).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores comment-shaped changes in non-code files', () => {
|
||||
const result = analyzeStagedDiff(
|
||||
diff('config.json', [' "key": "value // not a comment"'])
|
||||
)
|
||||
expect(result.violation).toBe(false)
|
||||
expect(result.hasCommentAdd).toBe(false)
|
||||
expect(result.hasCodeChange).toBe(false)
|
||||
})
|
||||
|
||||
it('uses real new-file line numbers from the hunk header', () => {
|
||||
const patch = [
|
||||
'diff --git a/src/a.ts b/src/a.ts',
|
||||
'--- a/src/a.ts',
|
||||
'+++ b/src/a.ts',
|
||||
'@@ -40,3 +40,4 @@',
|
||||
' const before = 1',
|
||||
'+doWork() // inline',
|
||||
' const after = 2'
|
||||
].join('\n')
|
||||
const result = analyzeStagedDiff(patch)
|
||||
expect(result.commentAdds[0].line).toBe(41)
|
||||
})
|
||||
})
|
||||
315
scripts/check-comment-commits.ts
Normal file
315
scripts/check-comment-commits.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { execSync } from 'node:child_process'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export interface CommentSyntax {
|
||||
line: boolean
|
||||
block: boolean
|
||||
html: boolean
|
||||
}
|
||||
|
||||
const TS_LIKE: CommentSyntax = { line: true, block: true, html: false }
|
||||
const VUE_LIKE: CommentSyntax = { line: true, block: true, html: true }
|
||||
const BLOCK_ONLY: CommentSyntax = { line: false, block: true, html: false }
|
||||
|
||||
export function commentSyntaxForFile(filePath: string): CommentSyntax | null {
|
||||
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
|
||||
switch (ext) {
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
case 'mts':
|
||||
case 'cts':
|
||||
case 'js':
|
||||
case 'mjs':
|
||||
case 'cjs':
|
||||
case 'jsx':
|
||||
return TS_LIKE
|
||||
case 'vue':
|
||||
return VUE_LIKE
|
||||
case 'css':
|
||||
return BLOCK_ONLY
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScanState {
|
||||
inBlock: boolean
|
||||
inTemplate: boolean
|
||||
inHtml: boolean
|
||||
}
|
||||
|
||||
export function freshScanState(): ScanState {
|
||||
return { inBlock: false, inTemplate: false, inHtml: false }
|
||||
}
|
||||
|
||||
export interface LineScan {
|
||||
hasComment: boolean
|
||||
hasCode: boolean
|
||||
state: ScanState
|
||||
}
|
||||
|
||||
export function scanLine(
|
||||
text: string,
|
||||
state: ScanState,
|
||||
syntax: CommentSyntax
|
||||
): LineScan {
|
||||
let { inBlock, inTemplate, inHtml } = state
|
||||
let hasComment = false
|
||||
let hasCode = false
|
||||
let i = 0
|
||||
const n = text.length
|
||||
|
||||
while (i < n) {
|
||||
const c = text[i]
|
||||
const c2 = text[i + 1]
|
||||
|
||||
if (inBlock) {
|
||||
hasComment = true
|
||||
if (c === '*' && c2 === '/') {
|
||||
inBlock = false
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (inHtml) {
|
||||
hasComment = true
|
||||
if (c === '-' && c2 === '-' && text[i + 2] === '>') {
|
||||
inHtml = false
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (inTemplate) {
|
||||
hasCode = true
|
||||
if (c === '\\') {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (c === '`') {
|
||||
inTemplate = false
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (c === ' ' || c === '\t' || c === '\r') {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (c === '\\') {
|
||||
hasCode = true
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if (syntax.line && c === '/' && c2 === '/') {
|
||||
hasComment = true
|
||||
break
|
||||
}
|
||||
|
||||
if (syntax.block && c === '/' && c2 === '*') {
|
||||
inBlock = true
|
||||
hasComment = true
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
syntax.html &&
|
||||
c === '<' &&
|
||||
c2 === '!' &&
|
||||
text[i + 2] === '-' &&
|
||||
text[i + 3] === '-'
|
||||
) {
|
||||
inHtml = true
|
||||
hasComment = true
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
|
||||
if (c === "'" || c === '"') {
|
||||
hasCode = true
|
||||
i++
|
||||
while (i < n) {
|
||||
if (text[i] === '\\') {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (text[i] === c) {
|
||||
i++
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (c === '`') {
|
||||
hasCode = true
|
||||
inTemplate = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
hasCode = true
|
||||
i++
|
||||
}
|
||||
|
||||
return { hasComment, hasCode, state: { inBlock, inTemplate, inHtml } }
|
||||
}
|
||||
|
||||
export interface CommentAdd {
|
||||
file: string
|
||||
line: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DiffAnalysis {
|
||||
violation: boolean
|
||||
hasCommentAdd: boolean
|
||||
hasCodeChange: boolean
|
||||
commentAdds: CommentAdd[]
|
||||
}
|
||||
|
||||
const HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/
|
||||
|
||||
export function analyzeStagedDiff(diff: string): DiffAnalysis {
|
||||
let file: string | null = null
|
||||
let syntax: CommentSyntax | null = null
|
||||
let newState = freshScanState()
|
||||
let oldState = freshScanState()
|
||||
let newLine = 0
|
||||
|
||||
let hasCommentAdd = false
|
||||
let hasCodeChange = false
|
||||
const commentAdds: CommentAdd[] = []
|
||||
|
||||
for (const raw of diff.split('\n')) {
|
||||
if (raw.startsWith('diff --git ')) {
|
||||
file = null
|
||||
syntax = null
|
||||
continue
|
||||
}
|
||||
|
||||
if (raw.startsWith('+++ ')) {
|
||||
const target = raw.slice(4).trim()
|
||||
if (target === '/dev/null') {
|
||||
file = null
|
||||
syntax = null
|
||||
continue
|
||||
}
|
||||
file = target.replace(/^b\//, '')
|
||||
syntax = commentSyntaxForFile(file)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!file || !syntax) continue
|
||||
|
||||
const hunk = HUNK_HEADER.exec(raw)
|
||||
if (hunk) {
|
||||
newLine = Number(hunk[1])
|
||||
newState = freshScanState()
|
||||
oldState = freshScanState()
|
||||
continue
|
||||
}
|
||||
|
||||
if (raw.startsWith('--- ')) continue
|
||||
if (raw.startsWith('\\')) continue
|
||||
|
||||
const marker = raw[0]
|
||||
const content = raw.slice(1)
|
||||
|
||||
if (marker === ' ') {
|
||||
newState = scanLine(content, newState, syntax).state
|
||||
oldState = scanLine(content, oldState, syntax).state
|
||||
newLine++
|
||||
continue
|
||||
}
|
||||
|
||||
if (marker === '+') {
|
||||
const scan = scanLine(content, newState, syntax)
|
||||
newState = scan.state
|
||||
if (scan.hasComment) {
|
||||
hasCommentAdd = true
|
||||
commentAdds.push({ file, line: newLine, text: content.trim() })
|
||||
}
|
||||
if (scan.hasCode) hasCodeChange = true
|
||||
newLine++
|
||||
continue
|
||||
}
|
||||
|
||||
if (marker === '-') {
|
||||
const scan = scanLine(content, oldState, syntax)
|
||||
oldState = scan.state
|
||||
if (scan.hasCode) hasCodeChange = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
violation: hasCommentAdd && hasCodeChange,
|
||||
hasCommentAdd,
|
||||
hasCodeChange,
|
||||
commentAdds
|
||||
}
|
||||
}
|
||||
|
||||
function getStagedDiff(): string {
|
||||
return execSync('git diff --cached --no-color --no-ext-diff -U3', {
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 64 * 1024 * 1024
|
||||
})
|
||||
}
|
||||
|
||||
function reportViolation(commentAdds: CommentAdd[]): void {
|
||||
const shown = commentAdds.slice(0, 20)
|
||||
const lines = shown
|
||||
.map(({ file, line, text }) => ` ${file}:${line} ${text}`)
|
||||
.join('\n')
|
||||
const overflow =
|
||||
commentAdds.length > shown.length
|
||||
? `\n …and ${commentAdds.length - shown.length} more`
|
||||
: ''
|
||||
|
||||
process.stderr.write(
|
||||
[
|
||||
'Commit blocked: contains both comments and code changes',
|
||||
'',
|
||||
'New comments in this commit:',
|
||||
lines + overflow,
|
||||
'',
|
||||
'Due to LLM abuse, commits with both comments and code are forbidden.',
|
||||
'Delete the comments immediately.',
|
||||
'If the comments are actually required, they can be rewritten in a',
|
||||
'standalone commit.',
|
||||
''
|
||||
].join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const analysis = analyzeStagedDiff(getStagedDiff())
|
||||
if (!analysis.violation) return
|
||||
reportViolation(analysis.commentAdds)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const invokedDirectly = (() => {
|
||||
const entry = process.argv[1]
|
||||
if (!entry) return false
|
||||
try {
|
||||
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
if (invokedDirectly) main()
|
||||
Reference in New Issue
Block a user