> { extends = "Pattern"}
53 |
54 | Owners ::= Owner (SPACES Owner)*
55 | Owner ::= Email | NamedOwner
56 | NamedOwner ::= '@' OwnerName
57 |
58 | OwnerName ::= Team | UserName
59 | Team ::= OrgName '/' TeamName
60 |
61 | Email ::= UserName '@' Domain
62 |
63 | UserName ::= VALUE
64 | OrgName ::= VALUE
65 | TeamName ::= VALUE
66 | Domain ::= VALUE
67 |
68 | private meta list_macro ::= <> + ('/' <
> +) *
69 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/indexing/CodeownersEntryOccurrence.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.indexing
2 |
3 | import com.github.fantom.codeowners.OwnersReference
4 | import com.intellij.openapi.vfs.VirtualFile
5 | import com.intellij.openapi.vfs.VirtualFileManager
6 | import java.io.DataInput
7 | import java.io.DataOutput
8 | import java.io.IOException
9 | import java.io.Serializable
10 | import java.util.*
11 |
12 | @JvmInline
13 | value class RegexString(val regex: String) {
14 | override fun toString() = regex
15 | }
16 |
17 | @JvmInline
18 | value class OwnerString(val owner: String): Comparable {
19 | override fun compareTo(other: OwnerString): Int {
20 | return owner.compareTo(other.owner)
21 | }
22 |
23 | override fun toString() = owner
24 | }
25 |
26 | /**
27 | * Entry containing information about the [VirtualFile] instance of the codeowners file mapped with the collection
28 | * of codeowners entries with line numbers for better performance. Class is used for indexing.
29 | */
30 | @Suppress("SerialVersionUIDInSerializableClass")
31 | class CodeownersEntryOccurrence(val url: String, val items: List>) : Serializable {
32 |
33 | /**
34 | * Returns current [VirtualFile].
35 | *
36 | * @return current file
37 | */
38 | var file: VirtualFile? = null
39 | get() {
40 | if (field == null && url.isNotEmpty()) {
41 | field = VirtualFileManager.getInstance().findFileByUrl(url)
42 | }
43 | return field
44 | }
45 | private set
46 |
47 | companion object {
48 | @Synchronized
49 | @Throws(IOException::class)
50 | fun serialize(out: DataOutput, entry: CodeownersEntryOccurrence) {
51 | out.run {
52 | writeUTF(entry.url)
53 | writeInt(entry.items.size)
54 | entry.items.forEach {
55 | writeUTF(it.first.regex)
56 | writeInt(it.second.offset)
57 | writeInt(it.second.owners.size)
58 | it.second.owners.forEach { owner ->
59 | writeUTF(owner.owner)
60 | }
61 | }
62 | }
63 | }
64 |
65 | @Synchronized
66 | @Throws(IOException::class)
67 | fun deserialize(input: DataInput): CodeownersEntryOccurrence {
68 | val url = input.readUTF()
69 | val items = mutableListOf>()
70 |
71 | if (url.isNotEmpty()) {
72 | val size = input.readInt()
73 | repeat((0 until size).count()) {
74 | val pattern = RegexString(input.readUTF())
75 | val offset = input.readInt()
76 | val size = input.readInt()
77 | val owners = mutableListOf()
78 | repeat((0 until size).count()) {
79 | owners.add(OwnerString(input.readUTF()))
80 | }
81 | items.add(Pair(pattern, OwnersReference(owners, offset)))
82 | }
83 | }
84 | return CodeownersEntryOccurrence(url, items)
85 | }
86 | }
87 |
88 | override fun hashCode() = Objects.hash(url, items)
89 |
90 | override fun equals(other: Any?) = when {
91 | other !is CodeownersEntryOccurrence -> false
92 | url != other.url || items.size != other.items.size -> false
93 | else -> items.indices.find { items[it].toString() != other.items[it].toString() } == null
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/CodeownersParserDefinition.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.lang.kind.bitbucket
2 |
3 | import com.github.fantom.codeowners.file.type.CodeownersFileType
4 | import com.github.fantom.codeowners.lang.CodeownersFile
5 | import com.github.fantom.codeowners.lang.CodeownersLanguage
6 | import com.github.fantom.codeowners.lang.kind.bitbucket.parser.CodeownersParser
7 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes
8 | import com.intellij.lang.ASTNode
9 | import com.intellij.lang.ParserDefinition
10 | import com.intellij.lang.ParserDefinition.SpaceRequirements
11 | import com.intellij.openapi.diagnostic.Logger
12 | import com.intellij.openapi.project.Project
13 | import com.intellij.psi.FileViewProvider
14 | import com.intellij.psi.PsiElement
15 | import com.intellij.psi.TokenType
16 | import com.intellij.psi.tree.IFileElementType
17 | import com.intellij.psi.tree.TokenSet
18 |
19 | class CodeownersParserDefinition : ParserDefinition {
20 | override fun createLexer(project: Project) = CodeownersLexerAdapter()
21 |
22 | override fun getWhitespaceTokens() = WHITE_SPACES
23 |
24 | override fun getCommentTokens() = COMMENTS
25 |
26 | override fun getStringLiteralElements(): TokenSet = TokenSet.EMPTY
27 |
28 | override fun createParser(project: Project) = CodeownersParser()
29 |
30 | override fun getFileNodeType() = FILE
31 |
32 | override fun createFile(viewProvider: FileViewProvider) = when (viewProvider.baseLanguage) {
33 | is CodeownersLanguage -> (viewProvider.baseLanguage as CodeownersLanguage).createFile(viewProvider)
34 | else -> {
35 | LOGGER.trace("Creating generic codeowners file")
36 | CodeownersFile(viewProvider, CodeownersFileType.INSTANCE)
37 | }
38 | }
39 |
40 | override fun spaceExistenceTypeBetweenTokens(left: ASTNode, right: ASTNode) = SpaceRequirements.MAY
41 |
42 | override fun createElement(node: ASTNode): PsiElement = CodeownersTypes.Factory.createElement(node)
43 |
44 | companion object {
45 | private val LOGGER = Logger.getInstance(CodeownersParserDefinition::class.java)
46 | /** Whitespaces. */
47 | val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE)
48 |
49 | /** Section comment started with ## */
50 | val SECTIONS = TokenSet.create(CodeownersTypes.SECTION)
51 |
52 | /** Header comment started with ### */
53 | val HEADERS = TokenSet.create(CodeownersTypes.HEADER)
54 |
55 | /** Negation element - ! in the beginning of the entry */
56 | val NEGATIONS = TokenSet.create(CodeownersTypes.NEGATION)
57 |
58 | /** Brackets [] */
59 | // val BRACKETS = TokenSet.create(CodeownersTypes.BRACKET_LEFT, CodeownersTypes.BRACKET_RIGHT)
60 |
61 | /** Slashes / */
62 | val SLASHES = TokenSet.create(CodeownersTypes.SLASH)
63 |
64 | /** All values - parts of paths */
65 | val VALUES = TokenSet.create(CodeownersTypes.VALUE)
66 |
67 | /** All values - parts of paths */
68 | val NAMES = TokenSet.create(CodeownersTypes.NAME_)
69 |
70 | val CONFIG_NAMES = TokenSet.create(
71 | CodeownersTypes.DESTINATION_BRANCH,
72 | CodeownersTypes.CREATE_PULL_REQUEST_COMMENT,
73 | CodeownersTypes.SUBDIRECTORY_OVERRIDES,
74 | )
75 |
76 | val CONFIG_VALUES = TokenSet.create(
77 | CodeownersTypes.BRANCH_PATTERN,
78 | CodeownersTypes.ENABLE,
79 | CodeownersTypes.DISABLE,
80 | )
81 |
82 | /** Regular comment started with # */
83 | val COMMENTS = TokenSet.create(CodeownersTypes.COMMENT)
84 |
85 | /** Element type of the node describing a file in the specified language. */
86 | val FILE = IFileElementType(BitbucketLanguage.INSTANCE)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersCoverPatternInspection.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.codeInspection
2 |
3 | import com.github.fantom.codeowners.CodeownersBundle
4 | import com.github.fantom.codeowners.lang.CodeownersFile
5 | import com.github.fantom.codeowners.services.PatternCache
6 | import com.github.fantom.codeowners.util.Utils
7 | import com.intellij.codeInspection.InspectionManager
8 | import com.intellij.codeInspection.LocalInspectionTool
9 | import com.intellij.codeInspection.ProblemDescriptor
10 | import com.intellij.codeInspection.ProblemsHolder
11 | import com.intellij.openapi.progress.ProgressManager
12 | import com.intellij.openapi.util.io.FileUtil
13 | import com.intellij.openapi.vfs.VirtualFile
14 | import com.intellij.psi.PsiElement
15 | import com.intellij.psi.PsiFile
16 | import dk.brics.automaton.BasicOperations
17 |
18 | /**
19 | * Inspection tool that checks if earlier entries are covered by later ones.
20 | */
21 | class CodeownersCoverPatternInspection : LocalInspectionTool() {
22 |
23 | /**
24 | * Reports problems at file level. Checks if entries are covered by other entries.
25 | *
26 | * @param file current working file to check
27 | * @param manager [InspectionManager] to ask for [ProblemDescriptor]'s from
28 | * @param isOnTheFly true if called during on the fly editor highlighting. Called from Inspect Code action otherwise
29 | * @return `null` if no problems found or not applicable at file level
30 | */
31 | @Suppress("ComplexMethod", "NestedBlockDepth", "ReturnCount")
32 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? {
33 | val virtualFile = file.virtualFile
34 | if (!Utils.isInProject(virtualFile, file.project)) {
35 | return null
36 | }
37 | val codeownersFile = file as? CodeownersFile ?: return null
38 |
39 | val problemsHolder = ProblemsHolder(manager, file, isOnTheFly)
40 |
41 | val cache = PatternCache.getInstance()
42 |
43 | val rules = codeownersFile.getRules()
44 | val compiledRegexes =
45 | rules.map { rulePsi ->
46 | val glob = rulePsi.pattern.value
47 | cache.getOrCreateGlobRegexes2(glob)
48 | }
49 |
50 | val regexes = rules.zip(compiledRegexes)
51 |
52 | for ((idx, pivot) in regexes.withIndex()) {
53 | for ((rulePsi, current) in regexes.drop(idx + 1)) {
54 | ProgressManager.checkCanceled()
55 | // improper subsetOf
56 | if (BasicOperations.subsetOf(pivot.second, current)) {
57 | val coveredPattern = pivot.first.pattern
58 | val coveringPattern = rulePsi.pattern
59 | problemsHolder.registerProblem(
60 | coveredPattern,
61 | message(coveringPattern, virtualFile)
62 | )
63 | }
64 | }
65 | }
66 |
67 | return problemsHolder.resultsArray
68 | }
69 |
70 | override fun runForWholeFile() = true
71 |
72 | /**
73 | * Helper for inspection message generating.
74 | *
75 | * @param coveringPattern entry that covers message related
76 | * @param virtualFile current working file
77 | * otherwise
78 | * @return generated message [String]
79 | */
80 | private fun message(
81 | coveringPattern: PsiElement,
82 | virtualFile: VirtualFile
83 | ): String {
84 | val path = FileUtil.toSystemIndependentName(virtualFile.path)
85 | return CodeownersBundle.message(
86 | "codeInspection.coverPattern.message",
87 | """${coveringPattern.text}"""
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/search/ui/CodeownersSearchFilterDialog.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.search.ui
2 |
3 | import com.github.fantom.codeowners.CodeownersManager
4 | import com.github.fantom.codeowners.indexing.CodeownersEntryOccurrence
5 | import com.github.fantom.codeowners.search.Filter
6 | import com.intellij.openapi.components.service
7 | import com.intellij.openapi.project.Project
8 | import com.intellij.openapi.ui.ComboBox
9 | import com.intellij.openapi.ui.DialogWrapper
10 | import com.intellij.ui.CollectionComboBoxModel
11 | import com.intellij.ui.ScrollPaneFactory
12 | import java.awt.BorderLayout
13 | import java.awt.event.ItemEvent
14 | import javax.swing.BorderFactory
15 | import javax.swing.JComponent
16 | import javax.swing.JPanel
17 |
18 | class CodeownersSearchFilterDialog(
19 | project: Project,
20 | codeownersFiles: List, // must be not empty
21 | ) : DialogWrapper(project) {
22 | private var contentPane: JPanel
23 | private var codeownersFileChooser = CodeownersFileChooser(codeownersFiles)
24 |
25 | private var orsPanel: OrsPanel
26 |
27 | var result: Pair>>? = null
28 | private set
29 |
30 | private val manager = project.service()
31 |
32 | private fun getOwners() = codeownersFileChooser
33 | .getChosenFile()
34 | .let(manager::getMentionedOwners)
35 |
36 | private fun createOrsPanel(): JPanel {
37 | val container = JPanel().apply { layout = BorderLayout() }
38 | container.add(orsPanel, BorderLayout.NORTH)
39 | return container
40 | }
41 |
42 | init {
43 | orsPanel = OrsPanel(getOwners())
44 |
45 | val scrollPanel = ScrollPaneFactory.createScrollPane(createOrsPanel())
46 |
47 | codeownersFileChooser.addItemListener {
48 | if (it.stateChange == ItemEvent.SELECTED) {
49 | orsPanel = OrsPanel(getOwners())
50 | scrollPanel.setViewportView(createOrsPanel())
51 | scrollPanel.revalidate()
52 | scrollPanel.repaint()
53 | }
54 | }
55 |
56 | // setup content panel
57 | contentPane = JPanel().also { cp ->
58 | cp.layout = BorderLayout() // to not let nested panels stretch vertically
59 |
60 | cp.add(JPanel()
61 | .also {
62 | it.layout = BorderLayout() // to make combobox stretch horizontally
63 | it.border = BorderFactory.createTitledBorder("Codeowners file")
64 | it.add(codeownersFileChooser)
65 | },
66 | BorderLayout.NORTH,
67 | )
68 | cp.add(scrollPanel, BorderLayout.CENTER)
69 | }
70 |
71 | init()
72 | }
73 |
74 | override fun doOKAction() {
75 | result = Pair((codeownersFileChooser.getChosenFile()), orsPanel.getOrs())
76 | super.doOKAction()
77 | }
78 |
79 | override fun createCenterPanel(): JComponent {
80 | return contentPane
81 | }
82 | }
83 |
84 | private class CodeownersFileChooser(
85 | codeownersFiles: List,
86 | ): ComboBox() {
87 | init {
88 | toolTipText = "Select a CODEOWNERS file used to calculate ownership"
89 | model = CollectionComboBoxModel(codeownersFiles.map(::CodeownersEntryOccurrenceWrapper))
90 | }
91 |
92 | fun getChosenFile() = (selectedItem as CodeownersEntryOccurrenceWrapper).codeownersEntryOccurrence
93 |
94 | class CodeownersEntryOccurrenceWrapper(val codeownersEntryOccurrence: CodeownersEntryOccurrence) {
95 | override fun toString(): String {
96 | return codeownersEntryOccurrence.url
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow.
2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN.
3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information.
4 |
5 | name: Release
6 | on:
7 | release:
8 | types: [prereleased, released]
9 |
10 | jobs:
11 |
12 | # Prepare and publish the plugin to JetBrains Marketplace repository
13 | release:
14 | name: Publish Plugin
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: write
18 | pull-requests: write
19 | steps:
20 |
21 | # Free GitHub Actions Environment Disk Space
22 | - name: Maximize Build Space
23 | uses: jlumbroso/free-disk-space@main
24 | with:
25 | tool-cache: false
26 | large-packages: false
27 |
28 | # Check out the current repository
29 | - name: Fetch Sources
30 | uses: actions/checkout@v4
31 | with:
32 | ref: ${{ github.event.release.tag_name }}
33 |
34 | # Set up Java environment for the next steps
35 | - name: Setup Java
36 | uses: actions/setup-java@v4
37 | with:
38 | distribution: zulu
39 | java-version: 21
40 |
41 | # Setup Gradle
42 | - name: Setup Gradle
43 | uses: gradle/actions/setup-gradle@v4
44 | with:
45 | gradle-home-cache-cleanup: true
46 |
47 | # Set environment variables
48 | - name: Export Properties
49 | id: properties
50 | shell: bash
51 | run: |
52 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d'
53 | ${{ github.event.release.body }}
54 | EOM
55 | )"
56 |
57 | echo "changelog<> $GITHUB_OUTPUT
58 | echo "$CHANGELOG" >> $GITHUB_OUTPUT
59 | echo "EOF" >> $GITHUB_OUTPUT
60 |
61 | # Update the Unreleased section with the current release note
62 | - name: Patch Changelog
63 | if: ${{ steps.properties.outputs.changelog != '' }}
64 | env:
65 | CHANGELOG: ${{ steps.properties.outputs.changelog }}
66 | run: |
67 | ./gradlew patchChangelog --release-note="$CHANGELOG"
68 | cat CHANGELOG.md || true
69 |
70 | # Publish the plugin to JetBrains Marketplace
71 | - name: Publish Plugin
72 | env:
73 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
74 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
75 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
76 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
77 | run: ./gradlew publishPlugin
78 |
79 | # Upload artifact as a release asset
80 | - name: Upload Release Asset
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
84 |
85 | # Create a pull request
86 | - name: Create Pull Request
87 | if: ${{ steps.properties.outputs.changelog != '' }}
88 | env:
89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90 | run: |
91 | VERSION="${{ github.event.release.tag_name }}"
92 | BRANCH="changelog-update-$VERSION"
93 | LABEL="release changelog"
94 |
95 | git config user.email "action@github.com"
96 | git config user.name "GitHub Action"
97 |
98 | git checkout -b $BRANCH
99 | git commit -am "Changelog update - $VERSION"
100 | git push --set-upstream origin $BRANCH
101 |
102 | gh label create "$LABEL" \
103 | --description "Pull requests with release changelog update" \
104 | --force \
105 | || true
106 |
107 | gh pr create \
108 | --title "Changelog update - \`$VERSION\`" \
109 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \
110 | --label "$LABEL" \
111 | --head $BRANCH
112 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchScopeDescriptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.search
2 |
3 | import com.github.fantom.codeowners.CodeownersBundle
4 | import com.github.fantom.codeowners.CodeownersIcons
5 | import com.github.fantom.codeowners.CodeownersManager
6 | import com.github.fantom.codeowners.search.ui.CodeownersSearchFilterDialog
7 | import com.intellij.ide.util.scopeChooser.ScopeDescriptor
8 | import com.intellij.openapi.components.service
9 | import com.intellij.openapi.project.Project
10 | import com.intellij.psi.search.GlobalSearchScope
11 | import com.intellij.psi.search.SearchScope
12 |
13 | class CodeownersSearchScopeDescriptor(private val project: Project) : ScopeDescriptor(null) {
14 | private val manager = project.service()
15 |
16 | /**
17 | * It is a very dirty hack to avoid showing [CodeownersSearchFilterDialog] when user opens `Search Structurally` dialog.
18 | * The issue is that when it happens, IDEA uses [com.intellij.structuralsearch.plugin.ui.ScopePanel.SCOPE_FILTER]
19 | * to allow `com.intellij.ide.util.scopeChooser.ClassHierarchyScopeDescriptor` in selection list, but
20 | * filter out [com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope].
21 | * It does so by comparing [com.intellij.ide.util.scopeChooser.ScopeDescriptor.getDisplayName] value with
22 | * the message for key `scope.class.hierarchy`
23 | * and checking type of the scope returned by [com.intellij.ide.util.scopeChooser.ScopeDescriptor.getScope]
24 | * for all other descriptors:
25 | * ```java
26 | * private static final Condition SCOPE_FILTER =
27 | * (ScopeDescriptor descriptor) -> IdeBundle.message("scope.class.hierarchy").equals(descriptor.getDisplayName()) ||
28 | * !(descriptor.getScope() instanceof ModuleWithDependenciesScope); // don't show module scope
29 | * ```
30 | * So, since upon first call to [getScope] [cachedScope] is `null`, we will show the dialog.
31 | * The same would happen for `ClassHierarchyScopeDescriptor`, but it is handled by this `getDisplayName` comparison
32 | *
33 | * The solution is to have a [unsafeToShowDialog] flag, that will protect us from showing the dialog
34 | * on first invocation in this case, since we set it to `true` only after [getDisplayName] s invoked,
35 | * which is the first method invoked in this `SCOPE_FILTER`. All other methods' invocations will reset it to `false`.
36 | *
37 | * In the regular case, e.g., when selecting this scope in `Find in Files` dialog,
38 | * we will show it on the first invocation, because in this case it turns out [getDisplayName]
39 | * is not the last method invoked before [getScope], so when [getScope] is invoked in these cases,
40 | * [unsafeToShowDialog] is reset to `false` by some other method (including the first invocation of [getScope]).
41 | */
42 | private var unsafeToShowDialog = false
43 |
44 | private var cachedScope: SearchScope? = null
45 |
46 | override fun getDisplayName(): String {
47 | unsafeToShowDialog = true
48 | return CodeownersBundle.message("search.scope.name")
49 | }
50 |
51 | override fun getIcon() = CodeownersIcons.FILE.also { unsafeToShowDialog = false }
52 |
53 | override fun scopeEquals(scope: SearchScope?): Boolean {
54 | unsafeToShowDialog = false
55 | return cachedScope == scope
56 | }
57 |
58 | override fun getScope(): SearchScope? {
59 | if (unsafeToShowDialog) {
60 | unsafeToShowDialog = false
61 | return null
62 | }
63 | if (cachedScope == null) {
64 | val codeownersFiles = manager.getCodeownersFiles().ifEmpty { return null }
65 |
66 | val dialog = CodeownersSearchFilterDialog(project, codeownersFiles)
67 |
68 | if (!dialog.showAndGet()) {
69 | cachedScope = GlobalSearchScope.EMPTY_SCOPE
70 | return null
71 | }
72 |
73 | val result = dialog.result!! // it cannot be null
74 |
75 | val (codeownersFile, dnf) = result
76 |
77 | cachedScope = CodeownersSearchScope(project, CodeownersSearchFilter(codeownersFile, DNF(dnf)))
78 | }
79 |
80 | return cachedScope
81 | }
82 | }
--------------------------------------------------------------------------------
/src/main/grammars/bitbucket/CodeownersLexer.flex:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.lang.kind.bitbucket.lexer;
2 |
3 | import com.intellij.lexer.*;
4 | import com.intellij.psi.tree.IElementType;
5 |
6 | import static com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes.*;
7 |
8 | %%
9 |
10 | %{
11 | public CodeownersLexer() {
12 | this((java.io.Reader)null);
13 | }
14 | %}
15 |
16 | %public
17 | %class CodeownersLexer
18 | %implements FlexLexer
19 | %function advance
20 | %type IElementType
21 | %unicode
22 |
23 | //EOL=\R
24 | //WHITE_SPACE={SPACES}
25 |
26 | CRLF = "\r" | "\n" | "\r\n"
27 | LINE_WS = [\ \t\f]
28 | WHITE_SPACE = ({LINE_WS}*{CRLF}+)+
29 |
30 | HEADER = ###[^\r\n]*
31 | SECTION = ##[^\r\n]*
32 | COMMENT = #[^\r\n]*
33 | NEGATION = \!
34 | SLASH = \/
35 | ATATAT = @@@
36 | AT = @
37 | QUOTE = \"
38 |
39 | DESTINATION_BRANCH_PATTERN = CODEOWNERS.destination_branch_pattern
40 | CREATE_PULL_REQUEST_COMMENT = CODEOWNERS.toplevel.create_pull_request_comment
41 | SUBDIRECTORY_OVERRIDES = CODEOWNERS.toplevel.subdirectory_overrides
42 | ENABLE = enable
43 | DISABLE = disable
44 | BRANCH_PATTERN = [^\s]+
45 |
46 |
47 | RULE_FIRST_CHARACTER = [^#\ ]
48 | //VALUE=[^@/\s\/]+
49 | VALUE = ("\\\["|"\\\]"|"\\\/"|[^\[\]\r\n\/\s])+
50 | NAME_ = [^#@/\s\/]+
51 | //VALUES_LIST=[^@/]+
52 | SPACES = \s+
53 |
54 | %state IN_BRANCH_PATTERN, IN_TOPLEVEL_CONFIG, IN_PATTERN, IN_OWNERS, IN_TEAM_DEFINITION
55 |
56 | %%
57 | {
58 | {WHITE_SPACE} { yybegin(YYINITIAL); return CRLF; }
59 | {LINE_WS}+ { return WSS; }
60 | {HEADER} { return HEADER; }
61 | {SECTION} { return SECTION; }
62 | {COMMENT} { return COMMENT; }
63 | {NEGATION} { return NEGATION; }
64 |
65 | {ATATAT} { yypushback(yylength()); yybegin(IN_TEAM_DEFINITION); }
66 |
67 | {DESTINATION_BRANCH_PATTERN} { yybegin(IN_BRANCH_PATTERN); return DESTINATION_BRANCH; }
68 | {SUBDIRECTORY_OVERRIDES} { yybegin(IN_TOPLEVEL_CONFIG); return SUBDIRECTORY_OVERRIDES; }
69 | {CREATE_PULL_REQUEST_COMMENT} { yybegin(IN_TOPLEVEL_CONFIG); return CREATE_PULL_REQUEST_COMMENT; }
70 |
71 | {RULE_FIRST_CHARACTER} { yypushback(1); yybegin(IN_PATTERN); }
72 | }
73 |
74 | {
75 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; }
76 | {LINE_WS}+ { yybegin(IN_BRANCH_PATTERN); return WSS; }
77 | {BRANCH_PATTERN} { yybegin(IN_BRANCH_PATTERN); return BRANCH_PATTERN; }
78 | }
79 |
80 | {
81 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; }
82 | {LINE_WS}+ { yybegin(IN_TOPLEVEL_CONFIG); return WSS; }
83 | {ENABLE} { yybegin(IN_TOPLEVEL_CONFIG); return ENABLE; }
84 | {DISABLE} { yybegin(IN_TOPLEVEL_CONFIG); return DISABLE; }
85 | }
86 |
87 | {
88 | {AT} { yybegin(IN_TEAM_DEFINITION); return AT; }
89 | {NAME_} { yybegin(IN_TEAM_DEFINITION); return NAME_; }
90 | // {QUOTED_VALUE} { return QUOTED_VALUE; }
91 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; }
92 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; }
93 | }
94 |
95 | {
96 | // {QUOTE} { yybegin(IN_WS_ENTRY); return QUOTE; }
97 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; }
98 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; }
99 | {SLASH} { yybegin(IN_PATTERN); return SLASH; }
100 |
101 | // "@" { return AT; }
102 |
103 | {VALUE} { yybegin(IN_PATTERN); return VALUE; }
104 | // {SPACES} { return SPACES; }
105 | }
106 |
107 | // {
108 | // {LINE_WS}+ { yybegin(IN_OWNERS); return CRLF; }
109 | // {SLASH} { yybegin(IN_ENTRY); return SLASH; }
110 | // {VALUES_LIST} { yybegin(IN_WS_ENTRY); return VALUES_LIST; }
111 | //}
112 |
113 | {
114 | {COMMENT} { yybegin(YYINITIAL); return COMMENT; }
115 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; }
116 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; }
117 | {NAME_} { yybegin(IN_OWNERS); return NAME_; }
118 | // {SLASH} { return SLASH; }
119 | {AT} { yybegin(IN_OWNERS); return AT; }
120 | }
121 |
122 | //[^] { return BAD_CHARACTER; }
123 |
--------------------------------------------------------------------------------
/src/main/resources/messages/CodeownersBundle.properties:
--------------------------------------------------------------------------------
1 | name=Codeowners Plugin
2 |
3 | action.appendFile.entryExists=Entry "{0}" already exists
4 | action.appendFile.entryExists.in=in {0}
5 |
6 | codeInspection.coverPattern=Cover pattern
7 | codeInspection.coverPattern.message=#ref is covered by {0} #loc
8 | codeInspection.duplicateEntry=Duplicate entry
9 | codeInspection.duplicateEntry.message=#ref entry is defined more than once #loc
10 | codeInspection.group=Codeowners
11 | codeInspection.unusedPattern=Unused pattern
12 | codeInspection.unusedPattern.message=#ref This pattern doesn't cover any file or directory #loc
13 | codeInspection.metasymbolsUsage=Metasymbols usage
14 | codeInspection.metasymbolsUsage.leadingDoubleStar=#ref Leading **/ can be removed since there are no other filepath delimiters #loc
15 | codeInspection.metasymbolsUsage.consecutiveDoubleStars=#ref One of these double star parts can be removed, because they cover each other #loc
16 | codeInspection.relativeEntry=Relative entry
17 | codeInspection.relativeEntry.message=#ref entry contains relative path
18 | codeInspection.incorrectEntry=Incorrect entry
19 | codeInspection.incorrectEntry.message=#ref has incorrect syntax:\n{0} #loc
20 |
21 | quick.fix.remove.rule=Remove rule
22 | quick.fix.relative.entry=Remove relative part
23 |
24 | daemon.lineMarker.directory=Directory pattern
25 | daemon.missingCodeowners=Missing CODEOWNERS file in project
26 | daemon.missingGitignore.create=Create .gitignore
27 | daemon.cancel=Cancel
28 |
29 | highlighter.brackets=Brackets
30 | highlighter.comment=Comment
31 | highlighter.header=Header
32 | highlighter.negation=Negation
33 | highlighter.section=Section
34 | highlighter.slash=Slash
35 | highlighter.value=Value
36 | highlighter.name=Name
37 | highlighter.configValue=Config value
38 | highlighter.configName=Config name
39 | highlighter.unused=Unused entry
40 |
41 | codeowners.colorSettings.displayName=CODEOWNERS files
42 |
43 | outer.label=Outer ignore rules:
44 |
45 | projectView.ignored=Ignored (.ignore plugin)
46 | projectView.containsHidden={0} hidden
47 |
48 | template.container.global=Global templates (OS, IDE, ...)
49 | template.container.root=Languages, frameworks
50 | template.container.user=User templates
51 | template.container.starred=Starred templates
52 |
53 | tokenType./=
54 | tokenType.BRACKET_LEFT=
55 | tokenType.BRACKET_RIGHT=
56 | tokenType.COMMENT=
57 | tokenType.CRLF=
58 | tokenType.HEADER=
59 | tokenType.SECTION=
60 | tokenType.VALUE=
61 |
62 | settings.displayName=Codeowners File Support
63 | settings.general=General settings
64 | settings.general.missingCodeownersFile=Notify about &missing CODEOWNERS file in the project
65 | settings.general.ignoredColor=Edit ignored files text color && font
66 | settings.general.ignoredFileStatus=Enable ignored file status &coloring
67 | settings.general.insertAtCursor=Insert new owners entries at cursor &position
68 | settings.general.unignoreFiles=Enable unignore files group (add entries prefixed with !)
69 | settings.general.notifyIgnoredEditing=Inform about editing ignored file
70 | settings.userTemplates=User templates
71 | settings.userTemplates.noTemplateSelected=No template is selected.
72 | settings.userTemplates.dialogTitle=User Template
73 | settings.userTemplates.dialogDescription=Set new template name:
74 | settings.userTemplates.dialogError=Template name cannot be empty
75 | settings.userTemplates.default.name=Example user template
76 | settings.userTemplates.default.content=### Example user template\n\n# IntelliJ project files\n.idea\n*.iml\nout\ngen
77 | settings.languagesSettings=Languages settings
78 | settings.languagesSettings.table.name=Language
79 | settings.languagesSettings.table.newFile=Show in "New > .ignore file"
80 | settings.languagesSettings.table.enable=Enable ignoring
81 | action.exportTemplates=Export Templates
82 | action.exportTemplates.description=Export ignore templates to file
83 | action.exportTemplates.wrapper=Export Selected User Templates to File...
84 | action.exportTemplates.error=User templates export failed.
85 | action.exportTemplates.success=Exported templates: {0}
86 | action.exportTemplates.success.title=Ignore User Templates
87 | action.importTemplates=Import Templates
88 | action.importTemplates.description=Import ignore templates from file
89 | action.importTemplates.wrapper=Import Ignore Templates
90 | action.importTemplates.wrapper.description=Please select the user ignore templates file to import.
91 | action.importTemplates.success=Imported templates: {0}
92 | action.importTemplates.error=User templates import failed.
93 | action.group.by.codeowner = Codeowner
94 |
95 | notification.group=.ignore plugin
96 | notification.group.update=.ignore plugin update information
97 |
98 | status.bar.codeowners.widget.name=Codeowners
99 | search.scope.name=File ownership
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # intellij-codeowners Changelog
4 |
5 | ## Unreleased
6 |
7 | ## [v0.9.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.9.0) (2024-08-24)
8 |
9 | ### Added
10 |
11 | - Support IDEA 2024.2
12 |
13 | ### Fixed
14 |
15 | - Sync configuration files with template repository
16 |
17 | ### Removed
18 |
19 | - Support for IDEA versions older than 2024.2
20 |
21 | ## [v0.8.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.8.0) (2024-04-21)
22 |
23 | ### Added
24 |
25 | - Support IDEA 2024.1
26 |
27 | ### Fixed
28 |
29 | - Duplicate groups when grouping files by ownership in changelist, Pull Requests and usage views
30 | - Empty groups when grouping by ownership is combined with grouping by e.g. module or directory, in changelist or Pull Requests views
31 | - Opening `Search Structurally` dialog automatically opened `File ownership` scope dialog
32 |
33 | ### Removed
34 |
35 | - Support for IDEA versions older than 2024.1
36 |
37 | ## [v0.7.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.7.0) (2023-12-08)
38 |
39 | ### Added
40 |
41 | - Implement ownership-based search scope: build an ownership predicate in a DNF and filter files during search
42 | - Support IDEA 2023.3
43 |
44 | ## [v0.6.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.6.0) (2023-08-13)
45 |
46 | ### Added
47 |
48 | - Support IDEA 2023.2
49 | - Show ownership changes in Local Changes panel, after file/dir is moved/renamed
50 |
51 | ### Fixed
52 |
53 | - Exception on showing quick documentation (`F1`) for GitHub user name (`@user`) / team name (`@org/team`)
54 | - Allow trailing comments for GitHub syntax
55 | - AlreadyDisposedException was thrown when closing a project
56 |
57 | ### Removed
58 |
59 | - Support for IDEA versions older than 2023.2
60 |
61 | ## [v0.5.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.5.0) (2022-12-06)
62 |
63 | ### Added
64 |
65 | - Pattern overlap inspection: detect patterns that override other patterns earlier in file
66 | - Support IDEA 2022.3
67 |
68 | ### Fixed
69 |
70 | - Order of grouping by code owner and by file in search results
71 |
72 | ### Removed
73 |
74 | - Support for IDEA versions older than 2022.3
75 |
76 | ## [v0.4.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.4.1) (2022-11-03)
77 |
78 | ### Fixed
79 |
80 | - Resolving files from file patterns when CODEOWNERS file not in the repository root
81 | - Proper translation of file patterns starting with `**/` into regex on pattern cache cleanup
82 |
83 | ## [v0.4.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.4.0) (2022-09-15)
84 |
85 | ### Added
86 |
87 | - Support of file paths with spaces and `@` for GitHub syntax
88 |
89 | ### Fixed
90 |
91 | - Incorrect parsing of paths without owners (reset ownership) for GitHub syntax
92 | - Resolving files from file patterns
93 |
94 | ### Removed
95 |
96 | - Support for IDEA versions older than 2022.1
97 |
98 | ## [v0.3.5](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.5) (2022-07-05)
99 |
100 | ### Added
101 |
102 | - Support codeowners unsetting for Github files, see https://github.community/t/codeowners-file-with-a-not-file-type-condition/1423/9
103 | - Support IDEA 2022.2
104 |
105 | ### Fixed
106 |
107 | - Speedup file references resolution (navigation through file tree using CTRL-click on CODEOWNERS file paths parts)
108 |
109 | ## [v0.3.4](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.4) (2022-03-29)
110 |
111 | ### Added
112 |
113 | - Structure view for Bitbucket files
114 | - Comment/uncomment actions
115 |
116 | ## [v0.3.3](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.3) (2022-03-20)
117 |
118 | ### Added
119 |
120 | - GoTo Team declaration for BitBucket files
121 | - Support IDEA 2022.1
122 |
123 | ## [v0.3.2](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.2) (2021-12-13)
124 |
125 | ### Added
126 |
127 | - Support `docs`, `.github`, `.bitibucket` dirs as CODEOWNERS file locations
128 | - Support bitbucket config lines
129 |
130 | ## [v0.3.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.1) (2021-11-25)
131 |
132 | ### Fixed
133 |
134 | - Bitbucket filetype detection
135 |
136 | ## [v0.3.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.0) (2021-08-09)
137 |
138 | ### Added
139 |
140 | - Group by owner in usage find results
141 |
142 | ## [v0.2.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.2.1) (2021-08-01)
143 |
144 | ### Fixed
145 |
146 | - Support IDEA 2021.2
147 |
148 | ## [v0.2.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.2.0) (2021-05-25)
149 |
150 | ### Added
151 |
152 | - Navigate from status bar to the line in CODEOWNERS file to know where code ownership is assigned
153 |
154 | ## [v0.1.0-eap.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.1.0) (2021-05-24)
155 |
156 | ### Added
157 |
158 | - Files syntax highlight (lexical)
159 | - Show owner of currently opened file in IDE status bar
160 | - Group file changes by owners
161 | - Navigation to entries in Project view
162 | - Navigation to Github user/team by ctrl-click on owner
163 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchFilter.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.search
2 |
3 | import com.github.fantom.codeowners.CodeownersManager
4 | import com.github.fantom.codeowners.OwnersList
5 | import com.github.fantom.codeowners.indexing.CodeownersEntryOccurrence
6 | import com.github.fantom.codeowners.indexing.OwnerString
7 | import com.intellij.openapi.vfs.VirtualFile
8 |
9 | interface Predicate {
10 | // TODO change to OwnersSet, duplicates make no sense
11 | fun satisfies(fileOwners: OwnersList?): Boolean
12 | }
13 |
14 | sealed interface Filter: Predicate {
15 | fun displayName(): CharSequence
16 |
17 | // TODO rewrite using service + context
18 | /**
19 | * @return false, if this filter definitely cannot be satisfied by given codeowners file, true otherwise
20 | */
21 | fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean
22 |
23 | sealed interface Condition: Filter {
24 | // files without assigned owners
25 | data object Unowned: Condition {
26 | override fun displayName() = "unowned"
27 |
28 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
29 | return true // cannot easily calculate it precise: need to proof there is at least one unowned file
30 | }
31 |
32 | override fun satisfies(fileOwners: OwnersList?) = fileOwners == null
33 | }
34 |
35 | // explicitly unowned files
36 | data object OwnershipReset: Condition {
37 | override fun displayName() = "explicitly unowned"
38 |
39 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
40 | return codeownersFile.items.any { it.second.owners.isEmpty() }
41 | }
42 |
43 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.isEmpty() ?: false
44 | }
45 |
46 | // files, owned by any (at least one) of the given owners
47 | data class OwnedByAnyOf(val owners: Set): Condition {
48 | override fun displayName() = "owned by any of ${owners.joinToString(", ")}"
49 |
50 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
51 | return true // because "owners" can be only from this file, so they own some files
52 | }
53 |
54 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.any { it in owners } ?: false
55 | }
56 |
57 | // files, owned by all the given owners, and maybe by some extra owners
58 | data class OwnedByAllOf(val owners: Set): Condition {
59 | override fun displayName() = "owned by all of ${owners.joinToString(", ")}"
60 |
61 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
62 | return codeownersFile.items.map { it.second }.any { satisfies(it.owners) }
63 | }
64 |
65 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.containsAll(owners) ?: false
66 | }
67 |
68 | // files, owned by only given owners, no extra owners allowed
69 | data class OwnedByExactly(val owners: Set): Condition {
70 | override fun displayName() = "owned by exactly ${owners.joinToString(", ")}"
71 |
72 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
73 | return codeownersFile.items.map { it.second }.any { satisfies(it.owners) }
74 | }
75 |
76 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.let { owners == it.toSet() } ?: false
77 | }
78 | }
79 |
80 | // negation of some Condition
81 | data class Not(val condition: Condition): Filter {
82 | override fun displayName() = "not ${condition.displayName()}"
83 |
84 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean {
85 | // cannot calculate it in abstract: it depends on the nature of the "condition"
86 | return true
87 | }
88 |
89 | override fun satisfies(fileOwners: OwnersList?): Boolean {
90 | return !condition.satisfies(fileOwners)
91 | }
92 | }
93 | }
94 |
95 | // disjunction of conjunctions
96 | data class DNF(val filters: List>): Predicate {
97 | override fun satisfies(fileOwners: OwnersList?) = filters.any { conj -> conj.all { n -> n.satisfies(fileOwners) } }
98 |
99 | fun displayName() =
100 | filters.joinToString(" or ") { conj -> "(${conj.joinToString(" and ") { n -> n.displayName() }})" }
101 | }
102 |
103 | data class CodeownersSearchFilter(
104 | val codeownersFile: CodeownersEntryOccurrence,
105 | // DNF: disjunction of conjunctions
106 | private val dnf: DNF
107 | ): Predicate by dnf {
108 | fun displayName() = "${codeownersFile.url}: ${dnf.displayName()}"
109 |
110 | context(CodeownersManager)
111 | fun satisfies(file: VirtualFile): Boolean {
112 | val ownersRef = getFileOwners(file, codeownersFile).getOrNull() ?: return false
113 | val ownersList = ownersRef.ref?.owners
114 |
115 | return satisfies(ownersList)
116 | }
117 | }
--------------------------------------------------------------------------------
/src/main/grammars/bitbucket/Codeowners.bnf:
--------------------------------------------------------------------------------
1 | // https://mibexsoftware.atlassian.net/wiki/spaces/CODEOWNERS/pages/222822413/Usage
2 | // playground: https://mibexsoftware.bitbucket.io/codeowners-playground/
3 | {
4 | parserClass = "com.github.fantom.codeowners.lang.kind.bitbucket.parser.CodeownersParser"
5 | extends = "com.github.fantom.codeowners.lang.CodeownersElementImpl"
6 |
7 | psiClassPrefix = "Codeowners"
8 | psiImplClassSuffix = "Impl"
9 | psiPackage = "com.github.fantom.codeowners.lang.kind.bitbucket.psi"
10 | psiImplPackage = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl"
11 |
12 | elementTypeHolderClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes"
13 | elementTypeClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersElementType"
14 | tokenTypeClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTokenType"
15 | // tokenTypeClass = "com.github.fantom.codeowners.lang.CodeownersTokenType"
16 |
17 | tokens = [
18 | WSS = "regexp:\s+"
19 | CRLF = "regexp:[\s\r\n]+"
20 | HEADER = "regexp:###.*"
21 | SECTION = "regexp:##.*"
22 | COMMENT = "regexp:#.*"
23 | SLASH = "/"
24 | AT = "@"
25 | // TEAMNAME = "regexp:[\w-]+"
26 | // USERNAME = "regexp:[\w\d-]+"
27 | // DOMAIN = "regexp:\w+(\.\w+)+"
28 | // VALUE = "regexp:[^@/\s]+"
29 | VALUE = "regexp:(?![!#\s])(?![\[\]])(?:\\[\[\]]|//|[^\[\]/\s])+"
30 | NAME_ = "regexp:[^@/\s]+"
31 | // TODO maybe make it more precise, like https://stackoverflow.com/a/12093994/7286194
32 | BRANCH_PATTERN = "regexp:\S+"
33 | //QUOTED_VALUE = 'regexp:"([^@/]+)+"'
34 | // VALUES_LIST = 'regexp:[^@/]+'
35 | // SPACES = 'regexp:\s+'
36 | ]
37 |
38 | name("Pattern.*") = "pattern"
39 | implements("Pattern") = "com.github.fantom.codeowners.lang.CodeownersPatternBase"
40 | mixin("Pattern") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersPatternExtImpl"
41 | implements("Rule") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersRuleBase"
42 | mixin("Rule") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersRuleExtImpl"
43 | // mixin("NamedOwner") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersNamedOwnerExtImpl"
44 | }
45 |
46 | codeownersFile ::= item_ *
47 | private item_ ::= HEADER | SECTION | COMMENT | ConfigurationLine | value_item_ | CRLF
48 | private value_item_ ::= (Rule | TeamDefinition) WSS? COMMENT?
49 |
50 | ConfigurationLine ::= DestinationBranchConfig | SubdirectoryOverridesConfig | CreatePullRequestCommentConfig
51 | DestinationBranchConfig ::= DESTINATION_BRANCH WSS BRANCH_PATTERN
52 | SubdirectoryOverridesConfig ::= SUBDIRECTORY_OVERRIDES WSS EnableOrDisable
53 | CreatePullRequestCommentConfig ::= CREATE_PULL_REQUEST_COMMENT WSS EnableOrDisable
54 | EnableOrDisable ::= ENABLE | DISABLE
55 |
56 | NEGATION ::= "!"
57 | Reset ::= NEGATION Pattern
58 |
59 | TeamDefinition ::= '@''@''@' TeamName WSS Owners {
60 | mixin = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersTeamDefinitionExtImpl"
61 | }
62 |
63 | Assign ::= Pattern /*SPACES*/ WSS Owners /*CRLF*/
64 |
65 | Rule ::= Reset | Assign
66 |
67 | Pattern ::= PatternDirectory | PatternFile
68 |
69 | //QuotedEntry ::= '"' <> '"'
70 | //private meta QuotedEntry::= '"' <> '"'
71 | //private meta entry_macro::= <> >> | <>
72 |
73 | //Entry ::= '/' ? <>
74 | //private meta entry_file_raw ::= '/' ? <>>>
75 | //private meta entry_dir_raw ::= '/' ? <>>> '/'
76 |
77 | PatternDirectory ::= '/' ? <> '/' //| ('"' '/' ? <> '/' '"') //{ extends = "EntryFile"}
78 | PatternFile ::= '/' ? <> //| ('"' '/' ? <> '"') //{ extends = "Entry"}
79 | //EntryDirectory ::= ('/' ? <> '/') | <> '/'>> //{ extends = "EntryFile"}
80 | //EntryFile ::= ('/' ? <>) | <> >> //{ extends = "Entry"}
81 |
82 | Owners ::= Owner (WSS Owner)*
83 | Owner ::= Email | NamedOwner
84 | NamedOwner ::= Team | User
85 |
86 | Team ::= '@''@' TeamName {
87 | // mixin = "CodeownersTeamNameNamedElementImpl"
88 | // implements = "CodeownersTeamNameNamedElement"
89 | mixin = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersNamedOwnerExtImpl"
90 | // methods = [ getName setName getNameIdentifier ]
91 | }
92 | User ::= '@' UserName
93 |
94 | Email ::= UserName '@' Domain
95 |
96 | // or make it token?
97 | //QuotedValue ::= '"'(CRLF VALUE)*'"'
98 |
99 | UserName ::= NAME_ //| QUOTED_VALUE
100 | TeamName ::= NAME_ //| QUOTED_VALUE
101 | Domain ::= NAME_
102 |
103 | private meta list_macro ::= <> + ('/' <
> +) *
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/indexing/CodeownersFilesIndex.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.indexing
2 |
3 | import com.github.fantom.codeowners.CodeownersBundle
4 | import com.github.fantom.codeowners.file.type.CodeownersFileType
5 | import com.github.fantom.codeowners.lang.CodeownersFile
6 | import com.intellij.openapi.application.ApplicationManager
7 | import com.intellij.openapi.diagnostic.Logger
8 | import com.intellij.openapi.project.DumbAware
9 | import com.intellij.openapi.project.Project
10 | import com.intellij.openapi.vfs.VirtualFile
11 | import com.intellij.util.indexing.*
12 | import com.intellij.util.indexing.FileBasedIndex.InputFilter
13 | import com.intellij.util.io.DataExternalizer
14 | import com.intellij.util.io.KeyDescriptor
15 | import java.io.DataInput
16 | import java.io.DataOutput
17 | import java.io.IOException
18 | import java.util.*
19 |
20 | /**
21 | * Implementation of [AbstractCodeownersFilesIndex] that allows to index all codeowners files content using native
22 | * IDE mechanisms and increase indexing performance.
23 | */
24 | @Suppress("TooManyFunctions")
25 | class CodeownersFilesIndex :
26 | FileBasedIndexExtension(),
27 | KeyDescriptor,
28 | DataIndexer,
29 | InputFilter,
30 | DumbAware {
31 | override fun getIndexer() = this
32 |
33 | override fun getKeyDescriptor() = this
34 |
35 | override fun isEqual(val1: CodeownersFileType, val2: CodeownersFileType) = val1 == val2
36 |
37 | override fun getHashCode(value: CodeownersFileType): Int = value.hashCode()
38 |
39 | override fun dependsOnFileContent() = true
40 |
41 | override fun getInputFilter() = this
42 |
43 | companion object {
44 | private val LOGGER = Logger.getInstance(CodeownersFilesIndex::class.java)
45 | val KEY = ID.create("CodeownersFilesIndex")
46 | private const val VERSION = 2
47 | private val DATA_EXTERNALIZER = object : DataExternalizer {
48 |
49 | @Throws(IOException::class)
50 | override fun save(out: DataOutput, entry: CodeownersEntryOccurrence) = CodeownersEntryOccurrence.serialize(out, entry)
51 |
52 | @Throws(IOException::class)
53 | override fun read(input: DataInput) = CodeownersEntryOccurrence.deserialize(input)
54 | }
55 |
56 | /**
57 | * Returns collection of indexed [CodeownersEntryOccurrence] for given [Project] and [CodeownersFileType].
58 | *
59 | * @param project current project
60 | * @param fileType filetype
61 | * @return [CodeownersEntryOccurrence] collection
62 | */
63 | fun getEntries(project: Project, fileType: CodeownersFileType): List {
64 | LOGGER.trace(">getEntries for file type $fileType in project ${project.name}")
65 | // try {
66 | if (ApplicationManager.getApplication().isReadAccessAllowed) {
67 | val scope = CodeownersSearchScope[project]
68 | val res = FileBasedIndex.getInstance().getValues(KEY, fileType, scope)
69 | LOGGER.trace(" = KEY
80 |
81 | @Suppress("ReturnCount")
82 | override fun map(inputData: FileContent): Map {
83 | val inputDataPsi = try {
84 | inputData.psiFile as? CodeownersFile
85 | } catch (e: Exception) {
86 | // if there is some stale indices
87 | // inputData.getPsiFile() could throw exception that should be avoided
88 | return emptyMap()
89 | } ?: return emptyMap()
90 |
91 | // val items = mutableListOf>>()
92 | // inputDataPsi.acceptChildren(
93 | // object : com.github.fantom.codeowners.lang.kind.github.psi.CodeownersVisitor() {
94 | // override fun visitPattern(entry: com.github.fantom.codeowners.lang.kind.github.psi.CodeownersPattern) {
95 | // val regex = entry.entryFile.regex(false)
96 | // items.add(Pair(RegexString(regex), entry.owners.ownerList.map{ OwnerString(it.text) }))
97 | // }
98 | // }
99 | // )
100 |
101 | val codeownersEntryOccurrence = CodeownersEntryOccurrence(inputData.file.url, inputDataPsi.getRulesList())
102 |
103 | return mapOf(
104 | CodeownersFileType.INSTANCE to codeownersEntryOccurrence,
105 | (inputData.fileType as CodeownersFileType) to codeownersEntryOccurrence,
106 | )
107 | }
108 |
109 | @Synchronized
110 | @Throws(IOException::class)
111 | override fun save(out: DataOutput, value: CodeownersFileType) {
112 | out.writeUTF(value.language.id)
113 | }
114 |
115 | @Synchronized
116 | @Throws(IOException::class)
117 | override fun read(input: DataInput): CodeownersFileType = // CodeownersFileType.INSTANCE
118 | input.readUTF().run {
119 | CodeownersBundle.LANGUAGES
120 | .asSequence()
121 | .map { it.fileType }
122 | .firstOrNull { it.languageName == this }
123 | .let { it ?: CodeownersFileType.INSTANCE }
124 | }
125 |
126 | override fun getValueExternalizer() = DATA_EXTERNALIZER
127 |
128 | override fun getVersion() = VERSION
129 |
130 | override fun acceptInput(file: VirtualFile) =
131 | file.fileType is CodeownersFileType // || CodeownersManager.FILE_TYPES_ASSOCIATION_QUEUE.containsKey(file.name)
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/fantom/codeowners/reference/CodeownersPatternsMatchedFilesCache.kt:
--------------------------------------------------------------------------------
1 | package com.github.fantom.codeowners.reference
2 |
3 | import com.github.benmanes.caffeine.cache.Cache
4 | import com.github.benmanes.caffeine.cache.Caffeine
5 | import com.github.fantom.codeowners.services.PatternCache
6 | import com.intellij.openapi.Disposable
7 | import com.intellij.openapi.application.ApplicationManager
8 | import com.intellij.openapi.components.Service
9 | import com.intellij.openapi.project.Project
10 | import com.intellij.openapi.vfs.VirtualFile
11 | import com.intellij.openapi.vfs.VirtualFileListener
12 | import com.intellij.openapi.vfs.VirtualFileManager
13 | import com.intellij.openapi.vfs.newvfs.BulkFileListener
14 | import com.intellij.openapi.vfs.newvfs.events.*
15 | import java.util.concurrent.ConcurrentHashMap
16 | import java.util.concurrent.TimeUnit
17 |
18 | /**
19 | * Cache that retrieves matching files using given glob prefix, taking at an level/dir only into account.
20 | * Cache population happened on demand in the background.
21 | * The cache eviction happen in the following cases:
22 | * * by using [VirtualFileListener] to handle filesystem changes
23 | * and clean cache if needed for the specific pattern parts.
24 | * * after entries have been expired: entries becomes expired if no read/write operations happened with the
25 | * corresponding key during some amount of time (10 minutes).
26 | * * after project dispose
27 | */
28 | typealias AtAnyLevel = Boolean
29 | typealias DirOnly = Boolean
30 | typealias Root = String
31 |
32 | @Service(Service.Level.PROJECT)
33 | internal class CodeownersPatternsMatchedFilesCache : Disposable {
34 | private val cacheByPrefix = ConcurrentHashMap, Collection>>()
35 |
36 | init {
37 | ApplicationManager.getApplication().messageBus.connect(this)
38 | .subscribe(
39 | VirtualFileManager.VFS_CHANGES,
40 | object : BulkFileListener {
41 | override fun after(events: List) {
42 | if (cacheByPrefix.isEmpty()) {
43 | return
44 | }
45 |
46 | for (event in events) {
47 | if (event is VFileCreateEvent ||
48 | event is VFileDeleteEvent ||
49 | event is VFileCopyEvent
50 | ) {
51 | cleanupCache(event.path)
52 | } else if (event is VFilePropertyChangeEvent && event.isRename) {
53 | cleanupCache(event.oldPath)
54 | cleanupCache(event.path)
55 | } else if (event is VFileMoveEvent) {
56 | cleanupCache(event.oldPath)
57 | cleanupCache(event.path)
58 | }
59 | }
60 | }
61 |
62 | private fun cleanupCache(path: String) {
63 | val caches = cacheByPrefix.filterKeys {
64 | path.startsWith(it)
65 | }
66 | // in practice there should be only one cache for given path
67 | caches.forEach { (root, cache) ->
68 | val relativePath = path.removePrefix(root).let {
69 | // TODO check if this condition can be false
70 | if (!it.endsWith('/')) {
71 | StringBuilder(it).append('/') // glob compiled to regex always contains trailing slash
72 | } else {
73 | it
74 | }
75 | }
76 | val cacheMap = cache.asMap()
77 | val globCache = PatternCache.getInstance()
78 | for (key in cacheMap.keys) {
79 | // TODO think about how to take the fact that path may point to a file into account.
80 | // In this case we shouldn't assume that atAnyLevel glob may point
81 | // to some subtree of this path and so shouldn't invalidate such a glob
82 | val regex = globCache.getOrCreatePrefixRegex(key.first, key.second, key.third)
83 | val match = regex.find(relativePath) ?: continue
84 | // if relative path matched only partially, it means
85 | // it points to a tree that is not covered by this glob,
86 | // even if they have common prefix
87 | if (match.range.last >= relativePath.indices.last) {
88 | cacheMap.remove(key)
89 | }
90 | }
91 | }
92 | }
93 | }
94 | )
95 | }
96 |
97 | override fun dispose() {
98 | cacheByPrefix.values.forEach { it.invalidateAll() }
99 | cacheByPrefix.clear()
100 | }
101 |
102 | private fun getCache(context: String) = cacheByPrefix.computeIfAbsent(context) {
103 | Caffeine.newBuilder()
104 | .expireAfterAccess(10, TimeUnit.MINUTES)
105 | .build() // CharSequence as a key to be able to lookup by substring without instantiation
106 | }
107 |
108 | fun getFilesByPrefix(context: String, prefix: CharSequence, atAnyLevel: Boolean, dirOnly: Boolean): Collection {
109 | return getCache(context)
110 | .getIfPresent(Triple(prefix, atAnyLevel, dirOnly)) ?: emptyList()
111 | }
112 |
113 | fun addFilesByPrefix(context: String, prefix: CharSequence, atAnyLevel: AtAnyLevel, dirOnly: DirOnly, files: Collection) {
114 | getCache(context).put(Triple(prefix, atAnyLevel, dirOnly), files)
115 | }
116 |
117 | companion object {
118 | @JvmStatic
119 | fun getInstance(project: Project): CodeownersPatternsMatchedFilesCache {
120 | return project.getService(CodeownersPatternsMatchedFilesCache::class.java)
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------