@sun-asterisk/sunlint 1.3.19 → 1.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,7 @@ function createCliProgram() {
37
37
  .option('--output-summary <file>', 'Output summary report file path (JSON format for CI/CD)')
38
38
  .option('--upload-report [url]', 'Upload summary report to API endpoint after analysis (default: Sun* Coding Standards API)')
39
39
  .option('--config <file>', 'Configuration file path (default: auto-discover)')
40
- .option('--github-annotate', 'Annotate GitHub PR with results (requires --output and --format=json)');
40
+ .option('--github-annotate [mode]', 'Annotate GitHub PR: annotate (inline), summary (comment), all (both) - default: all');
41
41
 
42
42
  // File targeting options
43
43
  program
@@ -135,6 +135,13 @@ CI/CD Integration:
135
135
  $ sunlint --all --output-summary=report.json --upload-report
136
136
  $ sunlint --all --output-summary=report.json --upload-report=https://custom-api.com/reports
137
137
 
138
+ GitHub Actions Integration:
139
+ $ sunlint --all --input=src --github-annotate # Both inline + summary (default)
140
+ $ sunlint --all --input=src --github-annotate=annotate # Inline comments only
141
+ $ sunlint --all --input=src --github-annotate=summary # Summary comment only
142
+ $ sunlint --all --input=src --github-annotate=all # Both inline + summary
143
+ $ sunlint --all --changed-files --github-annotate # With changed files
144
+
138
145
  ESLint Integration:
139
146
  $ sunlint --typescript --eslint-integration --input=src
140
147
  $ sunlint --all --eslint-integration --eslint-merge-rules --input=src
@@ -12,6 +12,7 @@ const { minimatch } = require('minimatch');
12
12
  class FileTargetingService {
13
13
  constructor() {
14
14
  this.supportedLanguages = ['typescript', 'javascript', 'dart', 'kotlin', 'java', 'swift'];
15
+ this.GitUtils = require('./git-utils');
15
16
  }
16
17
 
17
18
  /**
@@ -22,35 +23,43 @@ class FileTargetingService {
22
23
  try {
23
24
  const startTime = Date.now();
24
25
  const metadata = config._metadata;
25
-
26
+
26
27
  if (cliOptions.verbose) {
27
28
  console.log(chalk.cyan(`📁 File Targeting: ${this.getTargetingMode(metadata)}`));
28
29
  if (metadata?.shouldBypassProjectDiscovery) {
29
30
  console.log(chalk.blue(`đŸŽ¯ Optimized targeting for ${metadata.analysisScope}`));
30
31
  }
31
32
  }
32
-
33
+
33
34
  let allFiles = [];
34
-
35
- // Smart project-level optimization
36
- const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions);
37
-
38
- // Use enhanced targeting based on metadata
39
- if (metadata?.shouldBypassProjectDiscovery) {
40
- allFiles = await this.collectTargetedFiles(optimizedPaths, config, cliOptions);
35
+
36
+ // Handle --changed-files option (git diff mode)
37
+ if (cliOptions.changedFiles) {
38
+ if (cliOptions.verbose) {
39
+ console.log(chalk.cyan('🔄 Using --changed-files mode (git diff)'));
40
+ }
41
+ allFiles = await this.getGitChangedFiles(cliOptions);
41
42
  } else {
42
- allFiles = await this.collectProjectFiles(optimizedPaths, config, cliOptions);
43
+ // Smart project-level optimization
44
+ const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions);
45
+
46
+ // Use enhanced targeting based on metadata
47
+ if (metadata?.shouldBypassProjectDiscovery) {
48
+ allFiles = await this.collectTargetedFiles(optimizedPaths, config, cliOptions);
49
+ } else {
50
+ allFiles = await this.collectProjectFiles(optimizedPaths, config, cliOptions);
51
+ }
43
52
  }
44
53
 
45
54
  // Apply filtering logic
46
55
  const targetFiles = this.applyFiltering(allFiles, config, cliOptions);
47
-
56
+
48
57
  const duration = Date.now() - startTime;
49
-
58
+
50
59
  if (cliOptions.verbose) {
51
60
  console.log(chalk.green(`✅ File targeting completed in ${duration}ms (${targetFiles.length} files)`));
52
61
  }
53
-
62
+
54
63
  return {
55
64
  files: targetFiles,
56
65
  stats: this.generateStats(targetFiles, config),
@@ -62,6 +71,41 @@ class FileTargetingService {
62
71
  }
63
72
  }
64
73
 
74
+ /**
75
+ * Get files changed in git (for --changed-files option)
76
+ */
77
+ async getGitChangedFiles(cliOptions) {
78
+ try {
79
+ const baseRef = cliOptions.diffBase || null; // null = auto-detect
80
+ const changedFiles = this.GitUtils.getChangedFiles(baseRef);
81
+
82
+ if (cliOptions.verbose) {
83
+ const detectedBase = baseRef || this.GitUtils.getSmartBaseRef();
84
+ console.log(chalk.blue(`â„šī¸ Using base ref: ${detectedBase}`));
85
+ console.log(chalk.blue(`â„šī¸ Found ${changedFiles.length} changed file(s)`));
86
+
87
+ if (changedFiles.length > 0 && changedFiles.length <= 10) {
88
+ console.log(chalk.gray(' Changed files:'));
89
+ changedFiles.forEach(f => {
90
+ console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
91
+ });
92
+ } else if (changedFiles.length > 10) {
93
+ console.log(chalk.gray(` First 10 changed files:`));
94
+ changedFiles.slice(0, 10).forEach(f => {
95
+ console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
96
+ });
97
+ console.log(chalk.gray(` ... and ${changedFiles.length - 10} more`));
98
+ }
99
+ }
100
+
101
+ return changedFiles;
102
+ } catch (error) {
103
+ console.error(chalk.yellow(`âš ī¸ Failed to get changed files: ${error.message}`));
104
+ console.error(chalk.yellow('â„šī¸ Falling back to all files'));
105
+ return [];
106
+ }
107
+ }
108
+
65
109
  /**
66
110
  * Get targeting mode description
67
111
  */
@@ -293,15 +337,22 @@ class FileTargetingService {
293
337
  if (debug) console.log(`🔍 [DEBUG] applyFiltering: start with ${filteredFiles.length} files`);
294
338
  if (debug) console.log(`🔍 [DEBUG] config.include:`, config.include);
295
339
  if (debug) console.log(`🔍 [DEBUG] cliOptions.include:`, cliOptions.include);
340
+ if (debug) console.log(`🔍 [DEBUG] cliOptions.changedFiles:`, cliOptions.changedFiles);
341
+
342
+ // IMPORTANT: When using --changed-files, skip include patterns
343
+ // Git already filtered the files, we only need to apply excludes
344
+ const skipIncludePatterns = cliOptions.changedFiles;
296
345
 
297
346
  // 1. Apply config include patterns first (medium priority)
298
- if (config.include && config.include.length > 0) {
347
+ if (!skipIncludePatterns && config.include && config.include.length > 0) {
299
348
  filteredFiles = this.applyIncludePatterns(filteredFiles, config.include, debug);
300
349
  if (debug) console.log(`🔍 [DEBUG] After config include: ${filteredFiles.length} files`);
350
+ } else if (skipIncludePatterns && debug) {
351
+ console.log(`🔍 [DEBUG] Skipping config include patterns (--changed-files mode)`);
301
352
  }
302
353
 
303
354
  // 2. Apply CLI include overrides (highest priority - completely overrides config)
304
- if (cliOptions.include) {
355
+ if (!skipIncludePatterns && cliOptions.include) {
305
356
  // CLI include completely replaces config include - start fresh from all files
306
357
  filteredFiles = this.applyIncludePatterns([...files], cliOptions.include, debug);
307
358
  }
package/core/git-utils.js CHANGED
@@ -21,13 +21,98 @@ class GitUtils {
21
21
  }
22
22
  }
23
23
 
24
+ /**
25
+ * Detect if running in PR context (GitHub Actions, GitLab CI, etc.)
26
+ * @returns {Object|null} PR context info or null
27
+ */
28
+ static detectPRContext() {
29
+ // GitHub Actions
30
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request' ||
31
+ process.env.GITHUB_EVENT_NAME === 'pull_request_target') {
32
+ return {
33
+ provider: 'github',
34
+ baseBranch: process.env.GITHUB_BASE_REF,
35
+ headBranch: process.env.GITHUB_HEAD_REF,
36
+ prNumber: process.env.GITHUB_REF ? process.env.GITHUB_REF.match(/refs\/pull\/(\d+)\/merge/)?.[1] : null
37
+ };
38
+ }
39
+
40
+ // GitLab CI
41
+ if (process.env.CI_MERGE_REQUEST_ID) {
42
+ return {
43
+ provider: 'gitlab',
44
+ baseBranch: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
45
+ headBranch: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
46
+ prNumber: process.env.CI_MERGE_REQUEST_IID
47
+ };
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Get smart base reference for diff
55
+ * Auto-detects PR context or falls back to common base branches
56
+ * @param {string} cwd - Working directory
57
+ * @returns {string} Base reference for git diff
58
+ */
59
+ static getSmartBaseRef(cwd = process.cwd()) {
60
+ if (!this.isGitRepository(cwd)) {
61
+ throw new Error('Not a git repository');
62
+ }
63
+
64
+ // Check PR context first
65
+ const prContext = this.detectPRContext();
66
+ if (prContext && prContext.baseBranch) {
67
+ const candidates = [
68
+ `origin/${prContext.baseBranch}`,
69
+ `upstream/${prContext.baseBranch}`,
70
+ prContext.baseBranch
71
+ ];
72
+
73
+ for (const candidate of candidates) {
74
+ try {
75
+ execSync(`git rev-parse --verify ${candidate}`, { cwd, stdio: 'ignore' });
76
+ return candidate;
77
+ } catch (error) {
78
+ // Continue to next candidate
79
+ }
80
+ }
81
+ }
82
+
83
+ // Fallback to common base branches
84
+ const fallbackBranches = [
85
+ 'origin/main',
86
+ 'origin/master',
87
+ 'origin/develop',
88
+ 'upstream/main',
89
+ 'upstream/master',
90
+ 'main',
91
+ 'master',
92
+ 'develop'
93
+ ];
94
+
95
+ for (const branch of fallbackBranches) {
96
+ try {
97
+ execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
98
+ return branch;
99
+ } catch (error) {
100
+ // Continue to next candidate
101
+ }
102
+ }
103
+
104
+ // Last resort: use HEAD (uncommitted changes only)
105
+ return 'HEAD';
106
+ }
107
+
24
108
  /**
25
109
  * Get list of changed files compared to base reference
26
- * @param {string} baseRef - Base git reference (e.g., 'origin/main')
110
+ * @param {string|null} baseRef - Base git reference (e.g., 'origin/main'). If null, auto-detect.
27
111
  * @param {string} cwd - Working directory
112
+ * @param {boolean} includeUncommitted - Include uncommitted changes
28
113
  * @returns {string[]} Array of changed file paths
29
114
  */
30
- static getChangedFiles(baseRef = 'HEAD', cwd = process.cwd()) {
115
+ static getChangedFiles(baseRef = null, cwd = process.cwd(), includeUncommitted = true) {
31
116
  if (!this.isGitRepository(cwd)) {
32
117
  throw new Error('Not a git repository');
33
118
  }
@@ -35,15 +120,40 @@ class GitUtils {
35
120
  try {
36
121
  // Get git root directory
37
122
  const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
38
-
39
- const command = `git diff --name-only ${baseRef}`;
40
- const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
41
-
42
- return output
43
- .split('\n')
44
- .filter(file => file.trim() !== '')
45
- .map(file => path.resolve(gitRoot, file))
46
- .filter(file => fs.existsSync(file)); // Only existing files
123
+
124
+ // Auto-detect base ref if not provided
125
+ const actualBaseRef = baseRef || this.getSmartBaseRef(cwd);
126
+
127
+ const allFiles = new Set();
128
+
129
+ // Get committed changes
130
+ if (actualBaseRef !== 'HEAD') {
131
+ // Use two-dot diff for branch comparison (what's new in this branch)
132
+ const command = `git diff --name-only ${actualBaseRef}..HEAD`;
133
+ const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
134
+
135
+ output
136
+ .split('\n')
137
+ .filter(file => file.trim() !== '')
138
+ .forEach(file => allFiles.add(path.resolve(gitRoot, file)));
139
+ }
140
+
141
+ // Get uncommitted changes if requested
142
+ if (includeUncommitted) {
143
+ const uncommittedCommand = 'git diff --name-only HEAD';
144
+ try {
145
+ const uncommittedOutput = execSync(uncommittedCommand, { cwd: gitRoot, encoding: 'utf8' });
146
+ uncommittedOutput
147
+ .split('\n')
148
+ .filter(file => file.trim() !== '')
149
+ .forEach(file => allFiles.add(path.resolve(gitRoot, file)));
150
+ } catch (error) {
151
+ // Ignore errors for uncommitted changes (might be empty)
152
+ }
153
+ }
154
+
155
+ // Filter to only existing files
156
+ return Array.from(allFiles).filter(file => fs.existsSync(file));
47
157
  } catch (error) {
48
158
  throw new Error(`Failed to get changed files: ${error.message}`);
49
159
  }